Project structure & builds

A Day project combines a Cargo package, a Day.toml app manifest, and platform host projects. Rust code defines the app’s shared behavior; the host projects provide the files needed by Xcode, Gradle, and other platform build tools. The CLI builds them from the layout below.

The conventional project

my-app/
├── Day.toml                  # the app manifest: id, title, targets, window (name/version come from Cargo.toml)
├── Cargo.toml                # a normal Cargo package (bin + rlib)
├── build.rs                  # day-build codegen: the typed res:: constants src/lib.rs includes
├── src/
│   ├── lib.rs                # the app: pieces, signals, routes; res::locales::install()
│   └── main.rs               # desktop entry point; mobile entries live in lib.rs macros
├── resource/
│   ├── assets/               # arbitrary data files   → res::assets::stations_json
│   ├── images/               # processed images       → res::images::logo, logo@2x.png variants
│   ├── vectors/              # SVG glyphs, staged natively per backend → res::vectors::…
│   ├── fonts/                # custom fonts (.ttf/.otf), referenced by family name
│   ├── icons/                # the app icon master; `day prepare` renders every platform's set under build/day/host
│   └── locales/
│       ├── en/app.ftl        # Fluent translations, embedded at compile time; a new
│       └── fr/app.ftl        #   directory here is a new language (res::locales::install())
├── dayscript/                # dayscript flows: walkthroughs, screenshots, assertions
├── store/                    # the canonical store listing `day store` consumes
├── website/                  # optional app-site scaffold (skip with --no-website)
├── platform/
│   ├── ios/                  # Xcode scaffold: DayApp.xcodeproj + a thin Swift Runner
│   └── android/              # Gradle scaffold: settings/app modules, AndroidManifest, theme
├── platform/ohos/            # HarmonyOS scaffold: hvigor ArkTS host + sign-hap.mjs
└── build/day/                # generated: cargo target dirs, staged resources, screenshots
  • Day.toml is the single manifest. The app’s Day-specific identity (id, title, artifact, build), its declared targets, the default window geometry, and the [permissions], [signing], and [sbom] sections live here, while name and version are derived from Cargo.toml’s [package], so they can never drift. The identity properties (id, title, artifact, build) can be overridden per platform ([app.ios]), per toolkit ([app.qt]), or per target ([app.macos-appkit]); the platform scaffolds read the resolved values at build time.
  • The scaffolds are host projects. platform/ios, platform/android, and platform/ohos contain no app logic. Each is a minimal native shell that loads the Rust library and hands it the root view. They rarely change.
  • Generated files are written to build/day/: Cargo target directories (one per target and profile, so parallel builds never contend), staged resources, packed artifacts, and dayscript screenshots all live under one ignorable directory.

How a build works

Every target follows the same shape. day build -p <target> (or launch, which builds first) stages resources, selects the toolkit feature, and runs the platform’s build system for anything native:

day build -p <target>

├── 1. stage resources          resource/images + resource/assets → the target's native store
│                               (actool / aapt2 / GResource / .qrc / rawfile — see below)

├── 2. select features          --features <toolkit> + every standalone piece's
│                               <piece>/<toolkit> renderer feature (from cargo metadata)

└── 3. platform build
    ├── desktop   cargo build            → the app binary is the artifact
    ├── ios       xcodebuild             → Runner.app  (links the cargo staticlib)
    ├── android   cargo-ndk + gradle     → app.apk     (bundles the cargo cdylib)
    └── harmony   cargo + hvigor + sign  → app.hap     (bundles the cargo cdylib)

One backend is compiled per binary. The AppKit build contains no GTK code, the Android build only its JNI bridge. Standalone pieces (say, a Lottie or map piece) contribute their own native code and dependencies through Cargo metadata, so the app never re-declares per-piece build wiring.

Desktop: macos-appkit, linux-gtk, linux-qt, windows-xaml, and the GTK/Qt combinations

Desktop targets are the simplest: the artifact is the Cargo binary itself.

src/*.rs ──► cargo build -p my-app --features appkit     (per-target CARGO_TARGET_DIR)

                 ├── GTK: links system GTK 4 / libadwaita
                 ├── Qt / XAML: cc-compiled C++ shim (built by the toolkit crate's build.rs)
                 ├── XAML: embeds a side-by-side manifest (XAML Islands requires it)
                 └── macos-appkit: swift build prepass when Swift is contributed
                     (build/day/macos/DayPieces, statically linked — docs/swiftui)

         build/day/cargo/<target>/<profile>/my-app      ◄── day launch runs this directly

         day pack: macOS .app + ad-hoc codesign + .dmg

Because GTK and Qt are portable, macos-gtk, macos-qt, windows-gtk, and windows-qt build the same way on their respective hosts. Resources that need a native compiler (GResource, .qrc) are compiled if the tool is on PATH and otherwise fall back to filesystem loading, so a missing glib-compile-resources never fails the build.

iOS: ios-uikit

The Xcode project owns the bundle; the Rust code arrives as a static library through a build-phase callback into the day CLI:

day build -p ios-uikit

├── generate DayPieces           a local SwiftPM package assembled from every piece's
│                                [package.metadata.day.ios]: Swift shims, SwiftPM deps
│                                (remote or local packages), system frameworks, and the
│                                platform floor. A local package's public SwiftUI views
│                                are scanned and exported as crate::swiftui::… bindings;
│                                [package.metadata.day.macos] is the appkit twin

└── xcodebuild  platform/ios/DayApp.xcodeproj  (Runner target, iphonesimulator arm64)

        ├── script phase: "day xcode-backend build"
        │       └── cargo rustc --crate-type staticlib --target aarch64-apple-ios-sim
        │           → libmy_app.a, linked into Runner
        ├── actool: resource/images → Media.xcassets → optimized Assets.car
        └── Swift Runner: loads the Day root view, hands control to Rust

build/day/ios-uikit/Debug-iphonesimulator/MyApp.app

xcrun simctl install booted … && simctl launch          (day launch)

The callback design means opening platform/ios in Xcode and pressing Run also works, because the build phase calls back into day for the Rust half.

Xcode build settings

Both Xcode scaffolds keep their editable build settings in a committed platform/<ios|macos>/DayApp.xcconfig rather than inside the .xcodeproj, so they read and diff as plain text. Every build configuration uses it as its base configuration, and it ends with two optional includes:

#include? "../../build/day/xcconfig/ios.xcconfig"   # generated from Day.toml by `day build`
#include? "DayApp.local.xcconfig"                   # per-checkout, gitignored

The generated file carries the bundle id, version, and build number, so Day.toml stays authoritative once a build has run. DayApp.local.xcconfig is included after it and overrides both, which is where a signing team belongs:

DEVELOPMENT_TEAM = ABCDE12345
CODE_SIGN_STYLE = Automatic

The scaffold sets no signing keys of its own, so simulator and local desktop builds sign ad-hoc and need no Apple account. A device build takes the team from the local file, which every .gitignore covers, so a fork signs with its own team and its own bundle id without editing a tracked file. Settings that day build and day pack pass on the command line override all three.

Android: android-mdc

On Android, day runs Cargo first, then hands Gradle a project whose source sets already point at everything Day staged:

day build -p android-mdc

├── cargo-ndk (arm64-v8a) ────────► build/day/jniLibs/arm64-v8a/libmy_app.so

├── piece discovery ──────────────► build/day/android/day-pieces.json
│                                   (each piece's Java dirs, Gradle deps, Maven repos,
│                                    manifest permissions — read generically by Day's Gradle plugin)

└── gradle assembleDebug   platform/android/

        ├── sourceSets: the day-android Java shim + piece Java + jniLibs + assets/
        ├── aapt2: staged resource/images → res/drawable* → R.drawable ids
        └── Material 3 theme + DayActivity host (loads the .so, calls nativeStart)

platform/android/app/build/outputs/apk/debug/app-debug.apk

adb install … && am start DayActivity                   (day launch)

Day’s Gradle plugin configures that project from build/day/android/ (The Gradle project). A build started from Android Studio packages the Rust .so from the last day build.

HarmonyOS: harmony-arkui

The newest pipeline follows the Android shape with HarmonyOS tooling: an ArkTS host project in platform/ohos/, a cross-compiled NAPI library, and a post-build signing step with the public OpenHarmony development certificate:

day build -p harmony-arkui

├── cargo rustc --crate-type cdylib --target x86_64-unknown-linux-ohos   (emulator; arm64 device)
│       linker = $OHOS_NDK_HOME/llvm/bin/<triple>-clang
│       ────────► platform/ohos/entry/libs/<abi>/libentry.so

├── hvigor assembleHap   platform/ohos/   (ohpm install first)
│       ├── compiles the ArkTS host (Index.ets mounts Day via a NodeContent slot)
│       ├── packs libentry.so + resources/rawfile/day/ (staged images & assets)
│       └── → entry-default-unsigned.hap

└── sign-hap.mjs         patch compileSdkType → "OpenHarmony", sign with the SDK's
        │                public release material (no developer account required)

platform/ohos/entry/build/…/my-app-signed.hap

hdc install … && aa start EntryAbility                  (day launch)

Web: web-dom

This is the shortest of the five pipelines, and the only one with no host project to check in. The app’s lib crate is compiled straight to wasm and dropped next to the host page:

day build -p web-dom

├── cargo rustc --crate-type cdylib --target wasm32-unknown-unknown
│       exports day_dom_main (via day::day_start_web!)
│       ────────► dist/app.wasm

├── the host trio, embedded in the CLI and written out verbatim
│       ────────► dist/index.html · dist/shim.js · dist/day.css

└── bundled images and fonts + a fonts.json manifest the shim preloads
        ────────► dist/assets/…

              dist/ is the deployable — copy it to any static host

day launch -p web-dom   serves dist/ over loopback and opens your browser

There is no day pack for this target: dist/ is already the artifact. Browsers refuse to instantiate WebAssembly from file:, so use day launch (or any static server) rather than opening index.html directly.

How resources are packaged

resource/images/ and resource/assets/ are looked up by name at runtime through the generated typed constants (image(res::images::logo), resource(res::assets::stations_json); a typo is a compile error). Before each platform build Day stages the files, unchanged, into that target’s native resource store, so the platform’s machinery does the optimizing, and the runtime read is native (and zero-copy wherever the store exposes a stable pointer):

                        day build -p <target>
                                 │  stage
      ┌──────────────────────────┼──────────────────────────────┐
      ▼                          ▼                              ▼
   resource/images/logo.png   resource/assets/stations.json resource/icons/
      │                          │                              │
      │ per-target store         │ per-target store             │ dock / taskbar /
      │                          │                              │ launcher icon
┌─────┴──────────────────┐ ┌─────┴─────────────────────┐        │
│ iOS      Assets.car    │ │ Apple    bundle file+mmap │        ▼
│ macOS    bundle file   │ │ Android  AAssetManager    │   .icns / mipmap /
│ Android  res/drawable* │ │ GTK      GResource        │   .ico / xcassets
│ GTK      GResource     │ │ Qt       QResource        │
│ Qt       .qrc          │ │ XAML    loose file       │
│ XAML    scale-*.png   │ │ ArkUI    rawfile fd+mmap  │
│ ArkUI    rawfile       │ └───────────┬───────────────┘
└─────┬──────────────────┘             │
      ▼                                ▼
 image(res::images::logo)      resource(res::assets::stations_json)
 native by-name lookup         zero-copy &[u8] view, random access

At runtime, resource() returns a Resource backed directly by that store:

let res = day::resource(res::assets::stations_json).expect("bundled");
let bytes: &[u8] = res.as_slice();   // zero-copy view into the native store
let mut header = [0u8; 16];
res.read_at(0, &mut header);         // random access, no allocation

On Apple platforms that view is an mmap of the bundle file; on Android it is the NDK AAssetManager buffer of an uncompressed asset; GTK and Qt read out of resource blobs compiled into the binary; ArkUI maps the rawfile descriptor. Images resolve through each platform’s by-name API (UIImage(named:), R.drawable, gtk_picture_new_for_resource, QPixmap(":/…"), resource://RAWFILE/…), so density variants like logo@2x.png map onto the platform’s own scale-selection mechanism.

Fluent translations under resource/locales/ take a different path: they are embedded into the binary at compile time with include_str!, so locale switching never touches the filesystem.

The full per-platform details, including the limits (what gets optimized where, and which stores allow zero-copy), are in the resources reference; the HarmonyOS pipeline has its own reference page.