Apple

Once can build libraries, frameworks, applications, and test bundles for Apple platforms. This guide starts with one iOS application and a library it depends on, then queries, builds, runs, and tests that same project.

If you already have an Xcode project or workspace, you do not need to declare these targets by hand. Xcode Projects shows how to point Once at an existing .xcodeproj or .xcworkspace and get the same targets derived for you.

Prerequisites#

Apple targets require a macOS host with the Apple developer tools used by xcrun, swiftc, and clang. Verify that the Swift compiler is available:

bash
xcrun --find swiftc

Running this guide's iOS application also requires an installed Simulator runtime. Building a library does not require a running simulator.

Explicit Modules and Dependency Checks#

Set explicit_modules = true in an Apple target's attributes to turn imported Swift interfaces and Clang modules into separate cacheable build actions. Once first runs the Swift compiler's dependency scanner, builds the discovered modules, then compiles the target with implicit module building disabled. Independent module actions share the command's memory budget, and identical module actions can reuse results across targets. Scans use separate, cleared scratch directories. Module output paths include their compiler commands and input identities, so different modules cannot overwrite one another merely because their names match.

toml
[[target]]
name = "Core"
kind = "apple_library"
srcs = ["Sources/Core/**/*.swift"]
deps = ["Models"]
[target.attrs]
platform = "macos"
explicit_modules = true
dependency_check = "error"

The same attributes are available on Swift macros, frameworks, applications, executables, and test bundles. Set them on a swift_package_workspace or xcode_workspace seed to propagate them to resolved Swift targets. Native Xcode targets also honor SWIFT_ENABLE_EXPLICIT_MODULES = YES.

dependency_check defaults to "off". With "error", source imports of workspace modules must name direct dependencies, even when a transitive or inferred dependency makes the module available. System modules are exempt. An error identifies the consuming target, the missing module, and a suggested manifest repair. Checking requires explicit modules and a Swift compiler whose scan reports source-import information; unsupported compilers fail clearly.

Compiler binaries, development-kit contents, Clang resources, and host Swift plugins contribute to module cache identity. Changing a declared header or interface invalidates the corresponding module action. Existing projects keep implicit modules by default.

This is not a downloadable hermetic Apple toolchain. Builds still require the selected Xcode installation, and cached scanner commands retain its host paths. Module inputs outside the workspace and the identified development-kit and toolchain directories are rejected instead of being silently omitted from cache identity. Move such dependencies into the workspace or use implicit modules for those targets. Compiler scans that require auxiliary files created only inside their temporary cache are also rejected with a repair diagnostic. Keep interfaces and headers in the workspace, or leave explicit modules disabled for those targets. The scheduler currently reserves the default 250 mebibytes per module action. This is an admission estimate, not a hard limit on compiler memory use; large module builds can need more memory. Standalone C-family compilation is unchanged. Initial graph queries show the scan and deferred planning step; execution evidence records the module actions discovered at build time. Target scheduling still waits for dependency targets to complete, rather than releasing consumers as soon as one module is ready.

Declare the Application#

Create apps/Hello/once.toml with a reusable library and an application that depends on it:

toml
[[target]]
name = "AppCore"
kind = "apple_library"
srcs = ["Sources/AppCore/**/*.swift"]
[target.attrs]
platform = "ios"
minimum_os = "17.0"
[[target]]
name = "Hello"
kind = "apple_application"
srcs = ["Sources/HelloApp/**/*.swift"]
deps = ["./AppCore"]
[target.attrs]
platform = "ios"
bundle_id = "dev.once.Hello"
minimum_os = "17.0"
families = ["iphone"]

./AppCore resolves in the same package. The dependency is typed: apple_application accepts the linkable output produced by apple_library.

The smallest useful source layout is:

plaintext
apps/Hello/
├── once.toml
└── Sources/
├── AppCore/
└── HelloApp/

The application source needs an entry point, such as a Swift type marked with @main. The library contains code imported by the application.

Query Before Building#

Inspect the exact targets declared above:

bash
once query targets
once query capabilities apps/Hello/AppCore
once query capabilities apps/Hello/Hello
once query schema apple_application

AppCore exposes build. Hello exposes build and run. The schema query shows the application attributes, accepted dependencies, capabilities, and outputs without starting a compiler.

Build and Run#

Build the application bundle:

bash
once build apps/Hello/Hello

Once builds AppCore first because Hello depends on it. Outputs are materialized under .once/out/<target>/; the apple_application reference lists the exact output groups.

Launch the application in an iOS simulator:

bash
once run --visible apps/Hello/Hello

Once selects or boots a simulator, installs the application bundle, and launches its bundle identifier. The launch runs on every invocation instead of replaying from the action cache. Omit --visible when Simulator does not need to be brought to the foreground.

macOS applications use the host application launcher. Launching directly on an iPhone or iPad is not supported yet.

Create a Device-Specific Size-Analysis Archive#

Size-analysis services can report confusing results when they receive a universal application. Sentry, for example, does not thin an uploaded application and recommends thinning it before upload.

Add a device application and a package target for the device model you want to measure:

toml
[[target]]
name = "HelloDevice"
kind = "apple_application"
srcs = ["Sources/HelloApp/**/*.swift"]
deps = ["./AppCore"]
[target.attrs]
platform = "ios"
sdk_variant = "device"
bundle_id = "dev.once.Hello"
minimum_os = "17.0"
families = ["iphone"]
[[target]]
name = "HelloSizeAnalysis"
kind = "apple_thinned_package"
deps = ["./HelloDevice"]
[target.attrs]
device_model = "iPhone17,1"

Build the device-specific archive:

bash
once build apps/Hello/HelloSizeAnalysis

The target writes one or more archives with the .ipa extension and a stable manifest that maps them to the requested device model. Declare another apple_thinned_package target for each additional model so builds and cache entries stay independent.

These archives use ad-hoc signing and are intended for size analysis. They do not replace distribution signing or provisioning for physical-device installation. See the apple_thinned_package reference for the exact outputs and validation rules.

Add a Test Target#

Add a test target to the same apps/Hello/once.toml:

toml
[[target]]
name = "AppCoreTests"
kind = "apple_test_bundle"
srcs = ["Tests/AppCoreTests/**/*.swift"]
deps = ["./AppCore"]
[target.attrs]
platform = "ios"
minimum_os = "17.0"
swift_testing = true
labels = ["unit"]

Place tests that use Swift Testing under Tests/AppCoreTests/, then inspect and run the new capability:

bash
once query capabilities apps/Hello/AppCoreTests
once test apps/Hello/AppCoreTests

Apple tests currently support macOS logic tests and iOS simulator bundles. An application host discovered through dependency providers is supported, while direct test_host attributes, custom destinations, test plans, and device runners are not implemented. Non-empty unsupported attributes fail during graph analysis instead of being ignored. See the apple_test_bundle reference before adding those features.

Choose Values by Configuration#

Configurable attributes can use select. For example, a library shared by an iOS and macOS target can choose the platform framework it links:

toml
[target.attrs]
sdk_frameworks = { select = { ios = ["UIKit"], macos = ["AppKit"] } }

Apple configuration keys include platform names, architectures, simulator, device, mac_catalyst, combined keys such as ios:simulator, and default. When more than one branch matches, the most specific branch wins.

Attributes that determine the configuration, including platform, sdk_variant, archs, and mac_catalyst, must remain literal. The target kind schema identifies any other non-configurable attributes.

See the configurations guide for how --config overrides these tokens per invocation and how the effective configuration scopes outputs.

Connect Native Dependencies#

Apple targets can consume native outputs from other ecosystems through normal deps entries:

Add these only after the application builds on its own, then query the consumer again to confirm that the dependency contract is satisfied.

Supported Target Kinds and Limitations#

Use the target kind reference for the contract that matches the artifact:

Provisioning profiles, signing identities, and non-ad-hoc signing are accepted by the schema but are not supported yet. Using a non-empty value for one of these attributes fails validation before the build starts.

Next#

Continue with Memory once the application builds and tests. It shows how Once records durable context about graph work. If the application also contains Android or Rust code, use Ecosystems to choose the next independent integration.