Packaging & distribution

day pack turns a Day project into an installable package. It defaults to a release build, invokes the platform’s signing and packaging tools, and writes the output, checksum, and signing status to build/day/dist/.

The command runs from the project directory and requires the target’s packaging tools. For an Android APK:

day pack -t android-mdc --formats apk

Configure release credentials before distributing the result. Without them, Day can produce a development-signed or unsigned artifact; check the reported signing tier. If signing fails, start with the signing checks.

Build and install a release package on each target before distributing it. Configure signing credentials and check the destination store’s submission requirements.

Artifacts per target

TargetOutputDistribution
macos-appkit.dmg; .pkg with --formats pkgDisk image for direct download; installer package for the Mac App Store
ios-uikit.ipaDevice app archive
android-mdc.apk, .aabDirect installation or Google Play
linux-gtk, linux-qt.flatpak, .appimageFlatpak bundle or executable with bundled libraries
windows-winui.msix, -setup.exeSelf-contained MSIX package or per-user installer
windows-xaml (deprecated).msix, -setup.exeMSIX package or per-user installer
harmony-arkui.hapHarmonyOS application package
web-domStatic dist/ directoryStatic hosting; use day build

The development combinations macos-gtk, macos-qt, windows-gtk, and windows-qt do not have a day pack step. See platform support for their limitations.

macOS and iOS

For macOS, Day assembles the .app, signs its nested components with codesign, and creates a compressed UDZO disk image with an /Applications link. With release credentials configured, it signs the disk image, submits it through notarytool, and staples the notarization ticket. --formats pkg builds the Mac App Store package instead, and --formats dmg,pkg builds both from one build.

For iOS, with an App Store provisioning profile for the app id installed under ~/Library/MobileDevice/Provisioning Profiles/ and its distribution certificate in the keychain, Day builds the arm64 device app unsigned and then signs the .ipa with that profile and identity, exactly what day sign apply does for a package built elsewhere; xcodebuild’s own signing never runs, so nothing is provisioned or minted. Without a profile, Day runs xcodebuild archive with automatic signing through the App Store Connect key and exports the archive with a generated ExportOptions.plist using app-store-connect, which needs a developer’s Xcode account session. Without signing configuration, it produces <stem>-ios-uikit-unsigned.ipa for subsequent signing or sideloading.

Android

Day runs Gradle’s assembleRelease and bundleRelease with the configured signing settings. It verifies the APK with apksigner and checks 16 KB page alignment. Use the APK for direct installation and the AAB for Google Play submission.

Linux: Flatpak or AppImage

A Flatpak bundle uses a shared runtime: org.gnome.Platform for GTK or org.kde.Platform for Qt. The runtime is resolved from Flathub during installation. Install a bundle with:

flatpak install ./my-app-1.0-linux-gtk-x86_64.flatpak

The filename includes the toolkit so GTK and Qt bundles can coexist. The webview crate declares a Qt WebEngine BaseApp requirement, which Day includes when the binary links that engine. Other dependencies can declare their own Flatpak base requirements.

An AppImage bundles the toolkit libraries with the executable. Mark it executable before running it:

chmod +x ./my-app-1.0-linux-gtk-x86_64.appimage
./my-app-1.0-linux-gtk-x86_64.appimage

Day prepares an AppDir and invokes linuxdeploy with its GTK or Qt plugin. These plugins collect resources that a library dependency scan can miss, including image loaders, GIO modules, GSettings schemas, and Qt platform plugins.

Without the matching plugin, packaging can still succeed, but the AppImage requires the toolkit to be installed on the user’s machine. Day reports this during packaging.

Windows and HarmonyOS

Windows packaging uses makeappx and signtool for MSIX, and NSIS for the per-user -setup.exe installer. The NSIS installer does not require elevation, registers with Add/Remove Programs, and accepts /S for silent installation. A windows-winui package is self-contained: the Windows App SDK runtime travels inside the .msix and the installer, so users install nothing extra.

HarmonyOS packaging uses hvigor for a release build and hap-sign-tool for signing. Day uses configured release credentials when available, or the public development certificate otherwise.

Web

There is no web packaging step. Run day build -t web-dom and deploy the generated dist/ directory to a static host. It contains the HTML host page, JavaScript shim, stylesheet, WebAssembly module, images, and fonts.

Packaging options

Use --formats to select a subset of output formats, as in the Android example above. --profile defaults to release; another supported profile changes the build configuration, not whether the resulting signing tier is suitable for distribution. --no-sign skips signing and names the store package for a signer that runs later (<stem>-<target>-unsigned.ipa, .aab, .apk or .hap, which day sign apply takes as it is), and --no-notarize skips macOS notarization. To submit notarization without waiting for completion, pass --no-wait and check it later with day sign status <id>.

Artifact filenames follow this pattern:

<stem>[-<version>]-<target>[-<extra>].<ext>

For example: day-showcase-0.1.0-macos-appkit.dmg. Set the stem with [app] artifact in Day.toml, or override it with --artifact-name <stem>; Day converts the value to a filename slug. Use --no-version-in-name for a stable filename suitable for a releases/latest/download/<name> URL.

Signing configuration

Signing lives in Day.toml under [signing], with every secret referenced as ${ENV_VAR}; values resolve from the environment at pack time and are never stored in the manifest or printed by the tool:

[signing.macos]
identity = "${DAY_SIGN_MACOS_IDENTITY}"   # "Developer ID Application: …"

[signing.macos.notarize]
key-id = "${DAY_NOTARY_KEY_ID}"           # App Store Connect API key
issuer = "${DAY_NOTARY_ISSUER}"
key-path = "${DAY_NOTARY_KEY}"

[signing.macos.app-store]                 # the Mac App Store .pkg
identity = "${DAY_MAS_IDENTITY}"          # "Apple Distribution: …"
installer-identity = "${DAY_MAS_INSTALLER_IDENTITY}"  # "3rd Party Mac Developer Installer: …"
profile = "${DAY_MAS_PROFILE}"            # Mac App Store Connect .provisionprofile

[signing.ios]
team = "${DAY_APPLE_TEAM}"
key-id = "${DAY_ASC_KEY_ID}"              # ASC key for -allowProvisioningUpdates in CI
issuer = "${DAY_ASC_ISSUER}"
key-path = "${DAY_ASC_KEY}"

[signing.android]
keystore = "${DAY_ANDROID_KEYSTORE}"
key-alias = "${DAY_ANDROID_KEY_ALIAS}"
store-pass = "${DAY_KS_PASS}"
key-pass = "${DAY_KEY_PASS}"

[signing.windows]
provider = "self-signed-dev"              # or signtool-cert-store | azure-artifact-signing

[signing.ohos]
keystore = "${DAY_OHOS_KEYSTORE}"
key-alias = "${DAY_OHOS_KEY_ALIAS}"
store-pass = "${DAY_OHOS_KS_PASS}"
key-pass = "${DAY_OHOS_KEY_PASS}"
cert = "${DAY_OHOS_CERT}"
profile = "${DAY_OHOS_PROFILE}"

day sign check reports each platform’s readiness (env vars set, key files present) without printing any secret value.

Signing an existing package

day pack builds and signs in one step, which is what a project’s own CI wants. A distributor wants the two apart: build the submitted source with no credentials in reach, then sign the artifact that came out. Building runs the app’s own code — build scripts, Gradle plugins, Xcode run scripts — and signing runs none of it, so the credentials only ever meet a finished file.

day sign apply app-release.aab                 # signs in place
day sign apply app-release.apk --out signed.apk # leaves the input alone

The keys come from [signing.android], resolved strictly: an unset ${VAR} is an error naming the variable rather than a quiet drop to the dev keystore, because a dev-signed artifact looks finished and no store will take it. A distributor names its own key instead, and then the submitted app’s manifest is never read:

DAY_SIGN_STORE_PASS=… DAY_SIGN_KEY_PASS=… \
  day sign apply app-release.aab --keystore upload.keystore --key-alias upload

With both flags given there is no need for a project at all: day sign apply runs in a directory holding the package alone, which is how a submission queue keeps the app it signs away from the runner that holds the key. A .hap takes the same shape with its three files:

DAY_SIGN_STORE_PASS=… DAY_SIGN_KEY_PASS=… \
  day sign apply app-unsigned.hap --keystore release.p12 --cert app.cer \
    --profile app.p7b --key-alias release

The passwords stay in the environment, out of the argument list. .aab goes through jarsigner, .apk through zipalign then apksigner (v4 off, so no .idsig litter), and passwords reach both through the environment rather than the argument list, which every other process on the machine can read.

The signed copy is written beside the destination and moved into place only after it verifies, so a failure leaves the unsigned artifact exactly as it was. The report names both digests and the signing certificate, which is the field a store matches an upload against:

      Signed app-release.aab
      Digest 5f080ef8becf6015… → b9cc48e09326d9a1…
 Certificate sha256 98860a7b9c357cb68fee7d1cb6944238f8316a02ca75972ffc89b56e8ed68e24

--format json prints the same four fields as artifact, sha256, sha256_unsigned and certificate_sha256, which is what a pipeline records to tie a published binary to the build it came from.

Apple packages

An .ipa (or a bare .app, which comes back out as an .ipa) takes the steps Xcode’s export performs, spelled out because no project is present to drive it: embed the provisioning profile, sign every nested framework and plug-in before the bundle that contains them, sign the app, then repack with ditto.

day sign apply app.ipa \
  --profile "AppStore.mobileprovision" \
  --identity "Apple Distribution: Example Inc (ABCDE12345)"

--profile and --identity default to signing.ios.profile and signing.ios.identity. The flags matter more here than the Day.toml values do: a distributor signs an artifact built from someone else’s project, whose Day.toml names the developer’s profile and certificate rather than the distributor’s.

Entitlements come from the profile unless --entitlements <file> says otherwise. That is what Xcode does when a project overrides nothing, and it is the safe default in both directions: entitlements a profile does not grant are rejected at install, and asking for fewer than it grants silently drops capabilities the app shipped with. The profile’s set is what lands — application-identifier, aps-environment, beta-reports-active, get-task-allow=false for a distribution profile.

The signed bundle is verified with codesign --verify --deep --strict before it replaces anything, so a bad identity or an out-of-order nested signature fails with the artifact untouched. certificate_sha256 is the signing certificate itself, the same field the Android path reports.

Signing an Apple package needs macOS: codesign, security and ditto ship with Xcode. A macOS .app is signed for the Mac App Store into a .pkg. Signing an existing .dmg or .pkg is not implemented yet; day pack -t macos-appkit signs and notarizes the .dmg during the build.

The Mac App Store

The Mac App Store takes an installer package, not a disk image. The app inside is signed with an Apple Distribution identity over a Mac App Store provisioning profile, and the package is signed with a Mac Installer Distribution identity. The notarized .dmg and the store’s .pkg carry different signatures, so the two are separate packages built from the same app:

day pack -t macos-appkit --formats pkg          # build/day/dist/<stem>-macos-appkit.pkg
day pack -t macos-appkit --formats dmg,pkg      # the download and the store package together

The material comes from [signing.macos.app-store] in Day.toml (configuration). Without it, Day packs <stem>-macos-appkit-unsigned.pkg around the ad-hoc signed app and says so; the store refuses that package, and day sign apply signs the app inside it later.

To sign an app built elsewhere, as a release pipeline does after its build, give day sign apply the .app:

day sign apply "Day Showcase.app" \
  --profile ShowcaseMacAppStore.provisionprofile \
  --identity "Apple Distribution: Example Inc (ABCDE12345)" \
  --installer-identity "3rd Party Mac Developer Installer: Example Inc (ABCDE12345)" \
  --out day-showcase-macos-appkit.pkg

The flags default to the [signing.macos.app-store] values, and --out defaults to the bundle’s name with .pkg. The bundle is left as it was. Day signs a copy: it embeds the profile as Contents/embedded.provisionprofile, signs nested code before the bundle, signs the bundle with the hardened runtime and the app’s entitlements, verifies the result with codesign --verify --deep --strict, wraps it with productbuild, and checks the package with pkgutil --check-signature.

Entitlements come from the app. A Day build signs its sandbox policy into the bundle, even ad hoc, and that policy is what the store build keeps. Day adds the two identifiers the profile ties the signature to (com.apple.application-identifier and com.apple.developer.team-identifier) and drops the debugger grant. --entitlements <file> replaces the app’s set.

Before signing, Day checks what App Store Connect would otherwise reject after the upload:

  • The profile is a Mac App Store profile for this bundle id that has not expired. A development profile, a Developer ID profile, or an iOS profile is named as such.
  • The app is sandboxed.
  • Info.plist has LSApplicationCategoryType and a version.
  • Every device or personal-data entitlement has its usage description: the camera, the microphone, location, photos, contacts and calendars.
  • Every com.apple.developer.* capability the app asks for is granted by the profile.

Declare permissions in Day.toml [permissions] and Day writes their usage descriptions into platform/macos/Runner/Info.plist (permissions). Add the category to the same file:

<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>

The store listing and the upload lanes come from store/storefront.toml; see App Store Submissions.

Signing tiers

Every artifact carries a tier: release, dev-signed, or unsigned. When a ${VAR} is unset (a laptop without the release keys, a fork PR without repository secrets), day pack warns naming the variable and drops that platform to the dev tier (ad-hoc codesign on macOS, the fixed dev keystore embedded in the CLI on Android, a self-signed certificate on Windows, the unsigned device .ipa on iOS) instead of failing. The Android dev keystore is fixed, so dev builds stay byte-reproducible and an install from one machine can be upgraded by a build from another. The result JSON and the console both say so:

     Warning signing.macos.identity: ${DAY_SIGN_MACOS_IDENTITY} is not set — degrading to the dev signing tier
      Packed …/day-showcase-0.1.0-macos-appkit.dmg (dmg, dev-signed) sha256:ec51fa5f02ab…
     Warning day-showcase-0.1.0-macos-appkit.dmg is dev-signed — NOT distributable

A resolved configuration that is broken (a keystore path that doesn’t exist, a rejected notarization) is a hard failure with exit code 6.

Continuous integration

The GitHub Actions guide describes the intended app packaging and release workflow. Its reusable implementation lives outside this checkout and was not inspected in this repository-only review. Verify the workflow revision, artifact names, signing setup, and upload behavior before relying on it for distribution.

See App Store Submissions for iOS, Mac App Store and Android signing secrets, store records and upload lanes. macOS download signing and notarization use the shared workflow’s signing-environment input.

macOS App Sandbox

Enable [sandbox.macos-appkit] enabled = true in Day.toml to sandbox AppKit builds and packaged apps. See macOS sandboxing for configuration, native file access, persistent bookmarks, and App Store distribution limitations.

Separate configuration checks from distribution evidence

Use day pack --help to verify the installed version’s options and day doctor --toolkit <name> to diagnose its SDKs. Signing and store acceptance are separate from building the package.

Source: pack dispatch and packaging implementations. Keep credentials out of examples and logs. No signing, publishing, or store submission command was executed during this documentation work.