In 2008, the iPhone 3G and the first App Store introduced many people to mobile apps. We built AirBuild3G to give that phone something new to do. On an iPhone 3G or 3GS, you can describe a new app, work on its source with a coding model, and compile and run it on the same phone. The model runs on a service you choose. Your project files and the development tools stay on the phone.
AirBuild3G itself is built with Xcode 27 on a Mac. The apps you create with it use a separate compiler, installed on the phone. Making both work took changes in four areas: device preparation, linking, HTTPS, and the phone's own toolchain. The sections below follow that work through to your first app.
Preparing the Original Hardware
The small display and physical Home button make the iPhone 3G immediately recognizable. Building for it again means respecting its original proportions and limits, down to the controls that made its apps feel familiar. AirBuild3G itself uses the UIKit controls available on iOS 4, including grouped settings tables and navigation-bar actions.
AirBuild3G runs on the iPhone 3G and 3GS. The 3G requires armv6 binaries; the 3GS uses armv7. Firmware decides which environment package each phone can install. Our reference armv6 device is an iPhone 3G running iOS 4.2.1 with custom firmware.
- Preparerestore / activate
- ConnectSSH + network
- Installapp + environment package
- EnvironmentSettings → Environment
Start with a phone that boots to its Home screen and accepts SSH connections from your Mac. It needs a working network connection and package manager to receive and install the Debian packages. Its hardware model and firmware version decide which packages to send, and its network address decides where. You also need a reliable USB cable and power.
Some older phones need extra steps for activation and system access. For supported hardware, use Legacy iOS Kit and follow its guidance for your phone's model and firmware. Back up the phone before you begin. AirBuild3G itself does not activate the phone.
AirBuild3G also needs Substrate for its TLS support. On a 3G, the build must include armv6: an armv7-only package cannot run there, whatever its version. A phone prepared with Legacy iOS Kit may already have a compatible build. If Substrate works, leave it in place.
Settings → Environment follows the same rule. It adds development tools and leaves a working package manager, SSH service, and other base components alone. Upgrading those components wholesale can install binaries the old system cannot run.
Building with Xcode 27
Xcode 27's compiler still emits armv6 and armv7 code and accepts an iOS 4.0 deployment target. Xcode itself ships only the iOS 27 SDK, so we compile against a legacy SDK's headers. Pass the SDK's path as LEGACY_SDK on the make command line, or export it. The scripts that build the environment packages read it too, and a value set only inside the Makefile never reaches them.
The linker is stricter. It rejects armv6 as a link target, refuses any deployment target below iOS 4.3, and reports the legacy SDK's libraries as built for an unknown platform. For those libraries, we link against generated stubs instead and keep the legacy SDK for headers.
- Compiledclang
- arch
- armv6
- min iOS
- 4.0
- code
- e92d40f0 …
- Linkedrelabel, then ld
- arch
armv6armv7- min iOS
4.05.0- code
- e92d40f0 …
- Shippedrelabel back, patch
- arch
armv7armv6- min iOS
5.04.0- code
- e92d40f0 …
The build compiles both slices for iOS 4.0, links them for iOS 5.0, and then patches their minimum version back to 4.0. For armv6, it also relabels every object file as armv7 so the linker will take them, then relabels the linked slice back to armv6. Relabeling changes metadata, not instructions. The compiled code and the symbol stubs the linker adds must already run on armv6.
MH_MAGIC = 0xFEEDFACE
SUBTYPES = {"armv6": 6, "armv7": 9}
def main():
…
path, want = sys.argv[1], SUBTYPES[sys.argv[2]]
with open(path, "rb") as f:
buf = bytearray(f.read())
if struct.unpack_from("<I", buf, 0)[0] != MH_MAGIC:
…
struct.pack_into("<i", buf, 8, want)
with open(path, "wb") as f:
f.write(buf)- Mach-O's cpusubtype for each architecture: 6 is armv6, 9 is armv7.
- The whole relabel: four bytes at offset 8, the header's cpusubtype field. The instructions pass through untouched.
The build keeps -no_pie because iOS had no ASLR before 4.3. It also turns off Objective-C category merging, which would otherwise strip the Thumb bit from merged methods and crash AirBuild3G on a 3GS. make verify checks the architectures, the load commands, the armv6 instructions, and the Thumb bit on armv7 methods. A successful link proves none of this.
Run make to build AirBuild3G and its privileged helper. make payload downloads and verifies every pinned package into a local cache, and make package builds AirBuild3G's package and both environment packages from it, offline. Packaging needs LEGACY_SDK too, because each environment package carries an SDK archive for the phone.
make install-device DEVICE=root@<ip> installs AirBuild3G and the iOS 4 environment package, refreshes the Home screen, and restarts the app. For a 3GS on iOS 5 or later, pass ios6 as the third argument to tools/deploy_device.sh instead. The app runs as the mobile user, and a small helper beside it runs everything that needs root. The environment package is now on the phone; Settings → Environment installs the development tools it contains.
Connecting to a Model Service
In Settings, you enter three values under Endpoint: Base URL, API Key, and Model. Requests go directly from the phone to that endpoint. We run no server in between, and there is no account or device registration. Your model service runs the model, bills for usage, and tells you what to enter. If your phone was a gift from us, it connects to air.build instead.
Stock iOS 4 speaks only TLS 1.0 through SecureTransport, and a current endpoint may require TLS 1.2. We add TLS 1.2 with TLSFix, a Substrate tweak for CFNetwork.
The phone still has to verify the server's identity. Its old trust store cannot always build the certificate chain a current endpoint uses, and adding TLS 1.2 does not update that store. When a connection fails, treat encryption and certificate trust as separate problems.
AirBuild3G validates certificates by default. If a request fails, check the Base URL, API Key, Model, and certificate chain first. The Trust Any Certificate setting is an explicit exception. Turning it on removes a server-identity check and could expose your API key to anyone who can impersonate the endpoint. Leave it off unless the endpoint is on your own network. AirBuild3G reads the setting at every trust evaluation, so turning it off applies to the next request.
static OSStatus ABLSecTrustEvaluate(SecTrustRef trust, SecTrustResultType *result) {
…
OSStatus status = ABLOriginalSecTrustEvaluate(trust, result);
if (!ABLTrustAnyCertificate()) {
return status;
}
if (result != NULL) {
*result = kSecTrustResultUnspecified;
}
return errSecSuccess;
}- The phone's own evaluation runs first.
- Trust Any Certificate is off by default, so that answer goes back unchanged. The setting is read from
NSUserDefaultson every call.
Streaming a reply also has to fit the phone. AirBuild3G receives and decodes the stream on a background thread and passes only finished text to the interface. The transcript refreshes at an interval instead of remeasuring and scrolling for every token, which keeps layout work low while the reply arrives. A reply shows that the connection works. Compiling what the model writes still needs the environment on the phone.
Installing a Compiler the Phone Can Run
Settings → Environment installs the compiler, SDK headers, link libraries, and signing tools from the environment package. The iOS 4 package carries GCC for armv6; the package for iOS 5 and later carries clang for armv7. Each phone installs only the one that matches its firmware.
Every package is pinned by download address and checksum, and installation uses those exact files instead of asking a repository for the latest versions. Packages already installed at the pinned version are skipped, so a second run of Environment finishes right away. Installed packages are then held, so Cydia stops offering upgrades that break the toolchain.
For the iPhone 3G, the environment uses the 2008 build of iphone-gcc 4.2. The 2009 build reserves a gigabyte for precompiled headers, which pushes part of it beyond an armv6 phone's address space. Its compiler backends crash before they compile anything. Both builds identify as Apple build 5555, but only the 2008 one fits in that address space.
0x300000002008 iphone-gcc · every segment under 6 MBruns
512 MB array · test binaryruns
768 MB array · test binarySIGBUS
2009 iphone-gcc · 1 GB precompiled-header arenaSIGBUS
The SDK headers need changes too: this GCC cannot parse their block declarations. Before packaging the iOS 4 headers, we remove those declarations, along with every declaration that depends on a removed type. The clang package keeps Apple's headers unchanged, because clang supports blocks. Each package also ships a signing tool that can sign what its compiler produces.
The phone keeps its system frameworks in the dyld shared cache, so the framework directories hold no library files for a linker to read. We supply generated stubs that carry exported symbols and real library names. The linker resolves each reference against a stub and records which library it came from; at launch, dyld binds it to the real implementation in the shared cache. Because the stubs carry real symbol names, leaving out a framework fails the link instead of crashing the app at launch.
The environment also sets the include paths, points gcc and cc at the installed compiler, and exports link flags that pull in the Objective-C runtime and CoreFoundation. The bundled Makefile reads those settings, so a new project needs no toolchain setup of its own. From then on, your apps build on the phone itself.
build/%.o: src/%.m | build
@echo " compile $<"
@gcc -c $(IOS_CFLAGS) $< -o $@
# Compiling and linking take different flags, so they are separate steps.
# A missing -framework is a link error here, which is the point: link stubs
# with real symbol names are what make it one instead of a crash at launch.
build/$(NAME): $(OBJECTS)
@echo " link $@"
@gcc $(OBJECTS) -o $@ $(IOS_LDFLAGS) $(addprefix -framework ,$(FRAMEWORKS))
@# Unsigned is SIGKILL at launch, not a warning.
@ldid -S $@IOS_CFLAGSandIOS_LDFLAGScome from/etc/profile.d/airbuild.sh, which the environment installs, so no project names an SDK or compiler path.- The binary is signed on the phone too. Unsigned, it is killed at launch.
Creating and Running Your First App
A new project starts from the bundled UIKit template. AirBuild3G copies it into the project directory and puts its files in the model's context. The model starts from a project that already builds, so your first request can describe the app instead of asking for a Makefile.
A good first app is a button that increments a number on screen. It is small enough to read in full, yet it exercises editing, compiling, packaging, and running on the phone. In your request, name iOS 4 as the target and ask the model to build and install the app. Its interface code must stick to APIs the phone's iOS implements. A declaration in the headers does not mean the API exists at run time.
The model has four tools. Execute runs commands in the project directory. EditFile writes a complete file. PatchFile replaces an exact passage. CheckBuild runs make with the environment's settings loaded and returns the end of its output. Tool results return to the model, which can continue working or revise a failed change. Inference stays at your model service throughout.
tool call
tool result
Execute EditFile PatchFile CheckBuildThe loop repeats until the model replies without a tool call, you tap Stop, or 16 rounds have run.
Once the new app is installed, open it on the phone. For the counter, tapping the button and watching the number change checks exactly what you asked for. Later requests can change the layout or add another interaction, and the model builds on the existing files and earlier tool results.
AirBuild3G keeps each project and its conversation on the phone, saved after every tool round. It also remembers which project is open, because leaving the app or running low on memory can end its process. When you reopen AirBuild3G, it returns to that project, with its conversation and files. Your next message continues from the saved conversation, and the source is still there for the next edit and build.
On this screen, even that counter connects the apps you once used with the ones you can now make.
Something New on a Familiar Screen
The iPhone 3G arrived in 2008, just as the App Store opened. A new app could turn it into an instrument, a notebook, or a game you carried everywhere. The Home button brought you back to a grid of icons, each one an invitation to try something else.
On that phone today, you feel how long ago 2008 was. The display has not grown. The processor has not become faster. The original interface still asks apps to make careful use of a few controls and a little space. Working within those limits, you notice details that are easy to overlook on a newer device: whether a button responds, whether a conversation scrolls smoothly, whether the next screen appears when it should.
AirBuild3G gives that familiar screen one more job. A current model helps you write an app, and a compiler from the phone's own era builds it. The old icons can remain where they are.
Alongside them, there is room for an app that did not exist before.