diff --git a/Engine/overlay/src/omarchy_runtime.py b/Engine/overlay/src/omarchy_runtime.py index 094d336..28cb915 100644 --- a/Engine/overlay/src/omarchy_runtime.py +++ b/Engine/overlay/src/omarchy_runtime.py @@ -154,14 +154,15 @@ def run_layout( free_parts, # Also keep tight macOS containers for exact shortfalls. # Do not probe unrelated stubs or data-only containers that - # upstream did not consider resizable. + # upstream did not consider resizable. A stub carries the + # version of the macOS it was made from, so test stub too. [ part for part in installer.parts if part in resizable_parts or ( part.type == "Apple_APFS" and part.container is not None and part.os - and any(os.version for os in part.os) + and any(os.version and not os.stub for os in part.os) ) ] if is_gpt else [], stub_size, diff --git a/Engine/overlay/tests/test_omarchy_runtime.py b/Engine/overlay/tests/test_omarchy_runtime.py index 770692f..864df4d 100644 --- a/Engine/overlay/tests/test_omarchy_runtime.py +++ b/Engine/overlay/tests/test_omarchy_runtime.py @@ -366,11 +366,12 @@ def test_inspect_keeps_tight_containers_filtered_out_by_upstream(self): "OMARCHY_ENGINE_JOURNAL": str(self.journal_path), }) runtime.journal.inspection("apple,j314s", "supported") - macos = [SimpleNamespace(version="15.7")] + macos = [SimpleNamespace(version="15.7", stub=False)] tight = SimpleNamespace(type="Apple_APFS", container={"CapacityFree": 1}, os=macos) roomy = SimpleNamespace(type="Apple_APFS", container={}, os=macos) data = SimpleNamespace(name="data", type="Apple_APFS", container={}, os=[]) - stub = SimpleNamespace(type="Apple_APFS", container={}, os=[SimpleNamespace(version=None)]) + # Like upstream's OSInfo, a real stub reports its macOS version. + stub = SimpleNamespace(type="Apple_APFS", container={}, os=[SimpleNamespace(version="15.7", stub=True)]) upstream_data = SimpleNamespace(name="upstream-data", type="Apple_APFS", container={}, os=[]) recovery = SimpleNamespace(type="Apple_APFS_Recovery", container={}, os=macos) unknown = SimpleNamespace(type="Apple_APFS", container=None, os=macos) diff --git a/Engine/rebuild-python-overlay.py b/Engine/rebuild-python-overlay.py index b7ffc34..9b84b4d 100644 --- a/Engine/rebuild-python-overlay.py +++ b/Engine/rebuild-python-overlay.py @@ -15,7 +15,7 @@ BASE_SHA256 = '9e9277384b6c9e8b269cc79b1b24df7bfcdcbb898a596a677b74d1d18050aebe' BASE_COMMIT = 'f0469cea0899f3efed8efead604174c7a53c4451' -VERSION = 'v0.9.2-omarchy.20' +VERSION = 'v0.9.2-omarchy.27' _SPEC = importlib.util.spec_from_file_location( 'verify_source_lock', Path(__file__).resolve().parent / 'verify-source-lock.py') diff --git a/Engine/source-lock.json b/Engine/source-lock.json index 035594d..d527c55 100644 --- a/Engine/source-lock.json +++ b/Engine/source-lock.json @@ -141,7 +141,7 @@ } }, "downstream_overlay": { - "version": "v0.9.2-omarchy.20", + "version": "v0.9.2-omarchy.27", "capability": "candidate_bound_full_os_stage_one_authenticated_recovery", "engine_modes": [ "inspect", @@ -196,7 +196,7 @@ { "path": "overlay/src/omarchy_runtime.py", "destination": "src/omarchy_runtime.py", - "sha256": "bda8b7baa37052770da32a23b058da143b8b646aed1701866b3ee0338ff6729e" + "sha256": "8d5296fe17fe1bac6340a8c2433d0968f9d7f32f262e8a927dd507ec1efb823d" }, { "path": "overlay/src/omarchy_stage1.py", @@ -236,7 +236,7 @@ { "path": "overlay/tests/test_omarchy_runtime.py", "destination": "tests/test_omarchy_runtime.py", - "sha256": "d46c27fa44a74afcffdcbfa2ab64bc6ae470b8138760df181f28c013932d3cdc" + "sha256": "02f4a46d6159f7e189b6e2349dfe43184a6e99256a51a066dd95d02f0500ee57" }, { "path": "overlay/tests/test_omarchy_stage1.py", @@ -270,13 +270,13 @@ }, { "path": "rebuild-python-overlay.py", - "sha256": "f14f10690a2ae558ed8775e2c2ddb8e3ddc2f78f1d340f4899a18ecb47c3414a" + "sha256": "e4a6c4f9a3b5e9410d2511c16bf85899e9630713791acf8c59c669bce15e02d0" } ], "validation_artifact": { - "filename": "installer-v0.9.2-omarchy.20.tar.gz", - "size_bytes": 17839374, - "sha256": "7d7d87a934c128e501f8f6287d259195238ed1ae7953336b91738ff63514ea93", + "filename": "installer-v0.9.2-omarchy.27.tar.gz", + "size_bytes": 17839422, + "sha256": "4f9241b0139ba6ccdcfdb3484831002e07e36ba50274279c641ca2020593a15a", "reproducibility_scope": "two-identical-python-overlay-repacks-authenticated-base", "signature": "absent", "metadata_sha256": "2e6181ce6b6e17c11039e04bfadade8d10ae0e11889885ad8c9960b68f179a5d" diff --git a/Packaging/build-app.sh b/Packaging/build-app.sh index 93d2d1d..4cb7f38 100755 --- a/Packaging/build-app.sh +++ b/Packaging/build-app.sh @@ -46,8 +46,8 @@ helper_identifier="$INSTALLER_HELPER_IDENTIFIER" app_name="$INSTALLER_APP_NAME.app" app_executable_name="OmarchyAppleInstallerApp" daemon_plist_name="$helper_identifier.plist" -engine_file_name="installer-v0.9.2-omarchy.20.tar.gz" -engine_digest="7d7d87a934c128e501f8f6287d259195238ed1ae7953336b91738ff63514ea93" +engine_file_name="installer-v0.9.2-omarchy.27.tar.gz" +engine_digest="4f9241b0139ba6ccdcfdb3484831002e07e36ba50274279c641ca2020593a15a" if [[ $signing_identity == "-" ]]; then client_requirement="identifier \"$app_identifier\"" diff --git a/Sources/OmarchyAppleInstaller/InstallerAllocationRecommendation.swift b/Sources/OmarchyAppleInstaller/InstallerAllocationRecommendation.swift index f761ed6..bdd262d 100644 --- a/Sources/OmarchyAppleInstaller/InstallerAllocationRecommendation.swift +++ b/Sources/OmarchyAppleInstaller/InstallerAllocationRecommendation.swift @@ -59,19 +59,22 @@ public struct InstallerAllocationRecommendation: // The engine's hard container floor already protects macOS. A second // recommendation threshold can shrink the range as free space grows. let usable = available - reservedBytes + let margin = min( + usable / Self.resizeDriftMarginDivisor, + Self.maximumResizeDriftMarginBytes + ) + let marginCeiling = usable - margin + // The doubled size becomes the minimum only when it stays below the + // margin, so the divider keeps a range and the engine its drift room. if let recommended = candidate.recommendedInstallBytes { let alignedRecommended = Self.alignUp(recommended, unit: unit) - if alignedRecommended <= usable - (usable % unit) { + if alignedRecommended < marginCeiling - (marginCeiling % unit) { minimum = alignedRecommended } } - let margin = min( - usable / Self.resizeDriftMarginDivisor, - Self.maximumResizeDriftMarginBytes - ) // The margin is best effort: on a tight disk keep what fits above the - // minimum rather than dropping a candidate the reserve still allows. - maximum = min(usable, max(usable - margin, minimum)) + // partition floor rather than dropping a candidate the reserve allows. + maximum = min(usable, max(marginCeiling, minimum)) } else { return nil } diff --git a/Sources/OmarchyAppleInstaller/ValidationEngineArtifact.swift b/Sources/OmarchyAppleInstaller/ValidationEngineArtifact.swift index d1f1e70..9807888 100644 --- a/Sources/OmarchyAppleInstaller/ValidationEngineArtifact.swift +++ b/Sources/OmarchyAppleInstaller/ValidationEngineArtifact.swift @@ -18,11 +18,11 @@ /// An installation engine fix ships in a catalog without rebuilding /// and re-notarizing the app. Nothing may require them to be equal. public struct ValidationEngineArtifactLocator: Sendable { - public static let version = "v0.9.2-omarchy.20" - public static let fileName = "installer-v0.9.2-omarchy.20.tar.gz" + public static let version = "v0.9.2-omarchy.27" + public static let fileName = "installer-v0.9.2-omarchy.27.tar.gz" public static let expectedDigest = - "sha256:7d7d87a934c128e501f8f6287d259195238ed1ae7953336b91738ff63514ea93" - public static let expectedSizeBytes: UInt64 = 17_839_374 + "sha256:4f9241b0139ba6ccdcfdb3484831002e07e36ba50274279c641ca2020593a15a" + public static let expectedSizeBytes: UInt64 = 17_839_422 public init() {} diff --git a/Tests/OmarchyAppleInstallerTrustCoreTests/InstallerAllocationRecommendationTests.swift b/Tests/OmarchyAppleInstallerTrustCoreTests/InstallerAllocationRecommendationTests.swift index 129ab07..6244060 100644 --- a/Tests/OmarchyAppleInstallerTrustCoreTests/InstallerAllocationRecommendationTests.swift +++ b/Tests/OmarchyAppleInstallerTrustCoreTests/InstallerAllocationRecommendationTests.swift @@ -30,7 +30,13 @@ final class InstallerAllocationRecommendationTests: XCTestCase { let recommendation = try InstallerAllocationRecommendation( inventory: inventory, reservedBytes: 10 * gib ) - XCTAssertEqual(recommendation.minimumBytes, (available >= 72 ? 62 : 32) * gib) + // 62 GiB is adopted only once it fits under the 5% drift margin. + XCTAssertEqual(recommendation.minimumBytes, (available >= 76 ? 62 : 32) * gib) + // Near the floor the margin gives way so the install still fits; once + // the margin fits above the minimum, the divider can always move. + if (available - 10) * 19 > 32 * 20 { + XCTAssertGreaterThan(recommendation.maximumBytes, recommendation.minimumBytes) + } XCTAssertGreaterThanOrEqual(recommendation.maximumBytes, previousMaximum) XCTAssertLessThanOrEqual(recommendation.maximumBytes, (available - 10) * gib) previousMaximum = recommendation.maximumBytes @@ -41,6 +47,26 @@ final class InstallerAllocationRecommendationTests: XCTestCase { } } + func testRecommendedMinimumKeepsTheDriftMarginAndARange() throws { + // Each step of free space from just below to well above where the doubled + // size fits: the divider must move and the margin must survive whenever + // the doubled size is the minimum. + for quarters: UInt64 in 280...320 { + let available = quarters * gib / 4 + let resize = candidate( + kind: "resize", source: "disk0s2", length: 200 * gib, + minimumInstall: 32 * gib, minimumContainer: 200 * gib - available, + recommendedInstall: 62 * gib) + let recommendation = try InstallerAllocationRecommendation( + inventory: inventory([resize]), reservedBytes: 10 * gib) + let usable = available - 10 * gib + XCTAssertGreaterThan(recommendation.maximumBytes, recommendation.minimumBytes) + if recommendation.minimumBytes == 62 * gib { + XCTAssertLessThanOrEqual(recommendation.maximumBytes, usable - usable / 20) + } + } + } + func testLegacyRecommendationDoesNotShrinkCeilingOrFreezeAtFloor() throws { var previousMaximum: UInt64 = 0 for available: UInt64 in 31...100 { @@ -681,7 +707,8 @@ final class InstallerAllocationRecommendationTests: XCTestCase { length: UInt64, minimumInstall: UInt64, minimumContainer: UInt64 = 0, - identityDigest: String? = nil + identityDigest: String? = nil, + recommendedInstall: UInt64? = nil ) -> ValidatedEngineCandidate { ValidatedEngineCandidate( kind: kind, @@ -690,7 +717,8 @@ final class InstallerAllocationRecommendationTests: XCTestCase { lengthBytes: length, minimumInstallBytes: minimumInstall, minimumContainerBytes: minimumContainer, - identityDigest: identityDigest + identityDigest: identityDigest, + recommendedInstallBytes: recommendedInstall ) } } diff --git a/Tests/OmarchyAppleInstallerTrustCoreTests/ValidationEngineArtifactTests.swift b/Tests/OmarchyAppleInstallerTrustCoreTests/ValidationEngineArtifactTests.swift index 3509058..c7bb3d8 100644 --- a/Tests/OmarchyAppleInstallerTrustCoreTests/ValidationEngineArtifactTests.swift +++ b/Tests/OmarchyAppleInstallerTrustCoreTests/ValidationEngineArtifactTests.swift @@ -8,19 +8,19 @@ func testM3CapableInspectionIdentityIsPinnedExactly() { XCTAssertEqual( ValidationEngineArtifactLocator.version, - "v0.9.2-omarchy.20" + "v0.9.2-omarchy.27" ) XCTAssertEqual( ValidationEngineArtifactLocator.fileName, - "installer-v0.9.2-omarchy.20.tar.gz" + "installer-v0.9.2-omarchy.27.tar.gz" ) XCTAssertEqual( ValidationEngineArtifactLocator.expectedDigest, - "sha256:7d7d87a934c128e501f8f6287d259195238ed1ae7953336b91738ff63514ea93" + "sha256:4f9241b0139ba6ccdcfdb3484831002e07e36ba50274279c641ca2020593a15a" ) XCTAssertEqual( ValidationEngineArtifactLocator.expectedSizeBytes, - 17_839_374 + 17_839_422 ) } diff --git a/docs/extraction.md b/docs/extraction.md index c514c62..029cec0 100644 --- a/docs/extraction.md +++ b/docs/extraction.md @@ -73,4 +73,10 @@ The pinned archive is `installer-v0.9.2-omarchy.20.tar.gz`, 17,839,374 bytes, SH After merging current `main` on 2026-10-03, two repacks with `/usr/bin/python3` 3.9.6 again reproduced that exact pinned digest and size. A repack with Python 3.14.7 produced SHA-256 `e901371a4ba34d901a980b6f7586fc07d14234f9b50724ac4972e43ef35416af`, 17,839,417 bytes. All eight shipped downstream Python modules match the final repository sources byte for byte in both encodings, including the planner's disk-shortfall change. Reproduction must use the recorded interpreter before treating a different archive digest as stale source. The portable source-lock tests now check every downstream input and build recipe against its recorded digest, catching source changes without matching lock updates even when the upstream checkout is unavailable. -Ship installer **2.1.0 or later** together with the new engine and a signed catalog whose `installer.minimumVersion` is at least **2.1.0**. Both release-input templates carry this gate, and the catalog generator refuses a lower minimum for `.18` or newer engines in this lineage. Older installers then show the update-required message before decoding recommendation fields. The bundled inspection pins now select `.20`; signed production catalogs and frozen private-test catalogs are not changed by this source update. Artifact publication, catalog signing and physical installation qualification remain separate release steps. +## Stub probe and divider margin fixes + +Engine `.27` stops probing stub containers. A stub carries the version of the macOS it was made from, so a versioned OS alone admitted it to the limits probes, and one failed probe of a stub on a Mac with an existing Asahi or Omarchy install failed the whole inventory. The filter now admits only versioned OSes that are not stubs. The pinned archive is `installer-v0.9.2-omarchy.27.tar.gz`, 17,839,422 bytes, SHA-256 `4f9241b0139ba6ccdcfdb3484831002e07e36ba50274279c641ca2020593a15a`. Two repacks with macOS `/usr/bin/python3` 3.9.6 produced identical bytes from the authenticated `.14` base and locked v0.9.2 checkout; compared with `.20`, only `omarchy_runtime.py` and `version.tag` changed. The bundled inspection pins and both release-input templates select `.27`. + +Swift now withholds the resize drift margin before adopting the recommended (doubled) Omarchy size as the minimum. Adopting it first left a band of free-space states where the minimum equalled the maximum: the divider could not move and no margin remained for the engine's admission check. + +Ship installer **2.1.0 or later** together with the new engine and a signed catalog whose `installer.minimumVersion` is at least **2.1.0**. Both release-input templates carry this gate, and the catalog generator refuses a lower minimum for `.18` or newer engines in this lineage. Older installers then show the update-required message before decoding recommendation fields. The bundled inspection pins now select `.27`; signed production catalogs and frozen private-test catalogs are not changed by this source update. Artifact publication, catalog signing and physical installation qualification remain separate release steps. diff --git a/scripts/release-inputs-aurora.template.json b/scripts/release-inputs-aurora.template.json index 7c4ce65..2982bec 100644 --- a/scripts/release-inputs-aurora.template.json +++ b/scripts/release-inputs-aurora.template.json @@ -1,8 +1,8 @@ { "payload_name": "omarchy-2026.09.13-aarch64-apple-silicon-aurora-os-package.zip", - "engine_name": "installer-v0.9.2-omarchy.20.tar.gz", + "engine_name": "installer-v0.9.2-omarchy.27.tar.gz", "metadata_name": "installer_data.json", - "engine_version": "v0.9.2-omarchy.20", + "engine_version": "v0.9.2-omarchy.27", "evidence_revision": "4.0.3-mac.2.20260913-aurora", "asahi_installer_tag": "v0.9.2", "asahi_installer_revision": "dffbb38ef0c00c0431c609ecd8a00f42deb5b24c", @@ -47,6 +47,6 @@ "installer": { "minimum_version": "2.1.0", "latest_version": "2.1.0", - "download_url": "https://downloads.aicodelabs.com.au/omarchy-mac/installer/2.1.0/Omarchy-MX-Mac-Installer-2.1.0.pkg" + "download_url": "https://downloads.aicodelabs.com.au/omarchy-mac/installer/2.1.0/Omarchy-MX-Mac-Installer-2.1.0.zip" } } diff --git a/scripts/release-inputs.template.json b/scripts/release-inputs.template.json index 09d54b9..afe8cd5 100644 --- a/scripts/release-inputs.template.json +++ b/scripts/release-inputs.template.json @@ -1,8 +1,8 @@ { "payload_name": "omarchy-2026.09.13-aarch64-apple-silicon-asahi-os-package.zip", - "engine_name": "installer-v0.9.2-omarchy.20.tar.gz", + "engine_name": "installer-v0.9.2-omarchy.27.tar.gz", "metadata_name": "installer_data.json", - "engine_version": "v0.9.2-omarchy.20", + "engine_version": "v0.9.2-omarchy.27", "evidence_revision": "4.0.3-mac.1.20260913", "asahi_installer_tag": "v0.9.2", "asahi_installer_revision": "dffbb38ef0c00c0431c609ecd8a00f42deb5b24c", @@ -47,6 +47,6 @@ "installer": { "minimum_version": "2.1.0", "latest_version": "2.1.0", - "download_url": "https://downloads.aicodelabs.com.au/omarchy-mac/installer/2.1.0/Omarchy-MX-Mac-Installer-2.1.0.pkg" + "download_url": "https://downloads.aicodelabs.com.au/omarchy-mac/installer/2.1.0/Omarchy-MX-Mac-Installer-2.1.0.zip" } } diff --git a/test/shell.d/apple-installer-identity-test.sh b/test/shell.d/apple-installer-identity-test.sh index 6ce3694..01aa13d 100755 --- a/test/shell.d/apple-installer-identity-test.sh +++ b/test/shell.d/apple-installer-identity-test.sh @@ -77,7 +77,7 @@ import json, sys template, base, stem = sys.argv[1:4] installer = json.load(open(template))["installer"] version = installer["latest_version"] -expected = f"{base}/installer/{version}/{stem}-{version}.pkg" +expected = f"{base}/installer/{version}/{stem}-{version}.zip" if installer["download_url"] != expected: sys.exit(f"{template}: download_url {installer['download_url']} is not {expected}") PY