From d5572d2260aedfac8dbe68fdcdff9e18e61a4341 Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:53:20 -0700 Subject: [PATCH 1/3] Embed firmware helpers in Xcode app builds --- .github/workflows/ci.yml | 23 ++- .github/workflows/codeql.yml | 13 +- App/UITests/SmokeUITests.swift | 26 ++- .../Views/FirmwareView.swift | 1 + README.md | 13 +- .../Firmware/RestoreHelpers.swift | 2 +- iOSDeveloperToolkit.xcodeproj/project.pbxproj | 154 +++++++++++++++++- project.yml | 69 ++++++++ scripts/build-restore-helpers.sh | 65 +++++++- scripts/embed-restore-helpers.sh | 28 ++++ 10 files changed, 364 insertions(+), 30 deletions(-) create mode 100755 scripts/embed-restore-helpers.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2c39c6..7f2ee08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,10 @@ jobs: runs-on: macos-26 timeout-minutes: 30 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Select Xcode run: scripts/ci-select-xcode.sh - - uses: actions/cache@v6 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: .build key: spm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Package.resolved') }} @@ -58,9 +58,16 @@ jobs: runs-on: macos-26 timeout-minutes: 45 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Select Xcode run: scripts/ci-select-xcode.sh + - name: Install the firmware helpers' build tools + run: brew install autoconf automake libtool pkg-config cmake + - name: Reuse the firmware helpers built from the same pinned sources + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 + with: + path: build-output/restore-helpers + key: restore-helpers-${{ hashFiles('scripts/build-restore-helpers.sh') }}-${{ env.XCODE_VERSION }} - name: Build the app and UI tests with zero warnings # project.yml makes warnings errors, but Swift does not promote every diagnostic (for # example actor-isolation warnings involving XCTest), so also fail on any warning in @@ -85,7 +92,7 @@ jobs: scripts/check-layout.sh \ "$RUNNER_TEMP/dd/Build/Products/Debug/iOS Developer Toolkit (Swift).app/Contents/MacOS/iOS Developer Toolkit (Swift)" \ "$RUNNER_TEMP/layout" - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 if: always() with: name: ui-test-results @@ -99,10 +106,10 @@ jobs: runs-on: macos-26 timeout-minutes: 45 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Select Xcode run: scripts/ci-select-xcode.sh - - uses: actions/cache@v6 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: .build key: spm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Package.resolved') }} @@ -118,13 +125,13 @@ jobs: runs-on: macos-26 timeout-minutes: 90 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Select Xcode run: scripts/ci-select-xcode.sh - name: Install the firmware helpers' build tools run: brew install autoconf automake libtool pkg-config cmake - name: Reuse the firmware helpers built from the same pinned sources - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: build-output/restore-helpers key: restore-helpers-${{ hashFiles('scripts/build-restore-helpers.sh') }}-${{ env.XCODE_VERSION }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 132a86c..561373c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,10 +27,17 @@ jobs: packages: read security-events: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Select Xcode run: scripts/ci-select-xcode.sh - - uses: github/codeql-action/init@v4 + - name: Install the firmware helpers' build tools + run: brew install autoconf automake libtool pkg-config cmake + - name: Reuse the firmware helpers built from the same pinned sources + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 + with: + path: build-output/restore-helpers + key: restore-helpers-${{ hashFiles('scripts/build-restore-helpers.sh') }}-${{ env.XCODE_VERSION }} + - uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4 with: languages: swift build-mode: manual @@ -40,6 +47,6 @@ jobs: xcodebuild -project iOSDeveloperToolkit.xcodeproj -scheme iOSDeveloperToolkit \ -configuration Debug -destination 'platform=macOS' \ -derivedDataPath "$RUNNER_TEMP/dd" build - - uses: github/codeql-action/analyze@v4 + - uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4 with: category: "/language:swift" diff --git a/App/UITests/SmokeUITests.swift b/App/UITests/SmokeUITests.swift index 2a2d005..9051cff 100644 --- a/App/UITests/SmokeUITests.swift +++ b/App/UITests/SmokeUITests.swift @@ -10,6 +10,7 @@ final class SmokeUITests: XCTestCase { continueAfterFailure = false app = XCUIApplication() app.launchArguments = ["-ui-testing", "YES", "-demo-mode", "YES", "-ApplePersistenceIgnoreState", "YES"] + app.launchEnvironment["IDT_RESTORE_HELPERS"] = "" app.launch() app.activate() XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5), "The test app must be in the foreground before native UI interaction") @@ -38,6 +39,10 @@ final class SmokeUITests: XCTestCase { XCTFail("Missing workspace sidebar") return } + guard sidebar.isHittable else { + XCTFail("The app sidebar is obscured. Make the test app window visible before running native UI tests.") + return + } for _ in 0..<8 { let viewport = sidebar.frame let itemFrame = item.frame @@ -98,6 +103,23 @@ final class SmokeUITests: XCTestCase { XCTAssertTrue(app.descendants(matching: .any)["demo-banner"].waitForExistence(timeout: 10)) } + func testFirmwareToolsAreBundled() throws { + // The native UI test runner and app are sibling products in this build directory. + let appBundle = Bundle.main.bundleURL.deletingLastPathComponent() + .appendingPathComponent("iOS Developer Toolkit (Swift).app", isDirectory: true) + let helpers = appBundle.appendingPathComponent("Contents/Helpers", isDirectory: true) + for name in ["idevicerestore", "irecovery"] { + let helper = helpers.appendingPathComponent(name) + XCTAssertTrue(FileManager.default.isExecutableFile(atPath: helper.path), "Missing executable firmware helper: \(helper.path)") + } + let licenses = appBundle.appendingPathComponent("Contents/Resources/Licenses/restore-helpers", isDirectory: true) + for relativePath in ["SOURCES.txt", "idevicerestore/COPYING", "libirecovery/COPYING"] { + XCTAssertTrue(FileManager.default.isReadableFile(atPath: licenses.appendingPathComponent(relativePath).path), "Missing firmware license or source manifest: \(relativePath)") + } + openWorkspace("firmware") + XCTAssertFalse(app.descendants(matching: .any)["firmware.helper-problem"].exists, "The Firmware page must locate its bundled tools without an environment override") + } + func testEveryWorkspaceOpens() throws { let workspaces = ["overview", "device", "readiness", "location", "liveLogs", "actions", "apps", "backup", "installApp", "evidence", "externalTools", "help", "safety", "firmware", "securityAnalysis"] for workspace in workspaces { @@ -274,9 +296,7 @@ final class SmokeUITests: XCTestCase { } func testFirmwareRequiresValidationAndBlocksDemoInstall() throws { - let sidebar = app.descendants(matching: .any)["sidebar-firmware"] - XCTAssertTrue(sidebar.waitForExistence(timeout: 5)) - sidebar.click() + openWorkspace("firmware") let install = app.buttons["firmware.install"] XCTAssertTrue(install.waitForExistence(timeout: 5)) XCTAssertFalse(install.isEnabled, "A demo device cannot supply a validated install plan") diff --git a/App/iOSDeveloperToolkit/Views/FirmwareView.swift b/App/iOSDeveloperToolkit/Views/FirmwareView.swift index c6ac137..46a0f40 100644 --- a/App/iOSDeveloperToolkit/Views/FirmwareView.swift +++ b/App/iOSDeveloperToolkit/Views/FirmwareView.swift @@ -22,6 +22,7 @@ struct FirmwareView: View { WorkspacePage(workspace: .firmware) { if let problem = firmware.helperProblem { Label(problem, systemImage: "exclamationmark.triangle") + .accessibilityIdentifier("firmware.helper-problem") .font(.callout) .foregroundStyle(.orange) .fixedSize(horizontal: false, vertical: true) diff --git a/README.md b/README.md index 230d5f2..87bc527 100644 --- a/README.md +++ b/README.md @@ -372,16 +372,17 @@ xcodebuild -project iOSDeveloperToolkit.xcodeproj -scheme iOSDeveloperToolkit \ scripts/build-release.sh # universal, ad-hoc-signed release ZIP, SBOM, checksums in build-output/release/ ``` -The release includes the firmware helpers, which `scripts/build-release.sh` builds with -`scripts/build-restore-helpers.sh` from pinned sources. That needs -`brew install autoconf automake libtool pkg-config cmake`. To try the Firmware page from a -development build, build the helpers once and point the app at them: +Xcode Debug and Release builds include the firmware helpers. The `FirmwareHelpers` target +builds `idevicerestore` and `irecovery` from pinned sources, and the app target embeds them, +their licenses, and the source manifest. The first build needs the Homebrew build tools: ```bash -scripts/build-restore-helpers.sh -open --env IDT_RESTORE_HELPERS="$PWD/build-output/restore-helpers/out/bin" "iOS Developer Toolkit (Swift).app" +brew install autoconf automake libtool pkg-config cmake ``` +Later builds reuse the helpers while the build script and source pins are unchanged. +`scripts/build-restore-helpers.sh` also builds them independently for command-line tests. + The app icon and logo come from one image, `docs/brand/logo-source.png`: `xcrun swift scripts/generate-icons.swift` writes the app icon at every size, the in-app logo, and `docs/brand/` (`icon-1024.png`, `iOSDeveloperToolkitSwift.icns`, `logo-1024.png`, `logo-512.png`, diff --git a/Sources/ToolkitFeatures/Firmware/RestoreHelpers.swift b/Sources/ToolkitFeatures/Firmware/RestoreHelpers.swift index f33c518..3cde531 100644 --- a/Sources/ToolkitFeatures/Firmware/RestoreHelpers.swift +++ b/Sources/ToolkitFeatures/Firmware/RestoreHelpers.swift @@ -20,7 +20,7 @@ public enum RestoreHelper: String, CaseIterable, Sendable { for candidate in candidates where FileManager.default.isExecutableFile(atPath: candidate.path) { return candidate } - throw ToolkitError(.toolMissing, message: "The firmware tools are not included in this build.", recovery: "Use a release build of the app, or run scripts/build-restore-helpers.sh and set IDT_RESTORE_HELPERS to its bin folder.", technicalDetail: "Checked: " + candidates.map(\.path).joined(separator: ", ")) + throw ToolkitError(.toolMissing, message: "The firmware tools are not included in this build.", recovery: "Build the app with Xcode to embed the firmware tools, or run scripts/build-restore-helpers.sh and set IDT_RESTORE_HELPERS to its bin folder.", technicalDetail: "Checked: " + candidates.map(\.path).joined(separator: ", ")) } } diff --git a/iOSDeveloperToolkit.xcodeproj/project.pbxproj b/iOSDeveloperToolkit.xcodeproj/project.pbxproj index b4507de..0880dad 100644 --- a/iOSDeveloperToolkit.xcodeproj/project.pbxproj +++ b/iOSDeveloperToolkit.xcodeproj/project.pbxproj @@ -39,6 +39,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 513BA446F86D4DC4FF41E600 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8F9521A1C32BAC67A0066494 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 32D9773338414C6CCAE8E3EB; + remoteInfo = FirmwareHelpers; + }; 7D859C2B12ECAB79034F33D7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 8F9521A1C32BAC67A0066494 /* Project object */; @@ -59,7 +66,6 @@ 599C314C6B21A9A615B04C7A /* iOSDeveloperToolkit.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iOSDeveloperToolkit.entitlements; sourceTree = ""; }; 5FC5D72518E6E2101A0B339B /* iOSDeveloperToolkitApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSDeveloperToolkitApp.swift; sourceTree = ""; }; 64CADCDAC59205AFA532EF87 /* ActionsViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionsViews.swift; sourceTree = ""; }; - 6BF9453E787F70E21D026A1B /* security-review-candidate */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "security-review-candidate"; path = .; sourceTree = SOURCE_ROOT; }; 791078D9EF3EC6308839270F /* AppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModel.swift; sourceTree = ""; }; 7F724AF45C9EBAFFC75A5710 /* Components.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Components.swift; sourceTree = ""; }; 8C6560689EC51FF96E5F71E7 /* iOSDeveloperToolkit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSDeveloperToolkit.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -69,6 +75,7 @@ B26E835D20074A0489176A15 /* ReconnectGuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReconnectGuideView.swift; sourceTree = ""; }; B37EBEC3A6576895CA18665A /* DataViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataViews.swift; sourceTree = ""; }; B81EECBCB7D517436001EFF8 /* LogViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViews.swift; sourceTree = ""; }; + BF28B9563BB119B8B18C547A /* IOSDeveloperToolkit-Swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "IOSDeveloperToolkit-Swift"; path = .; sourceTree = SOURCE_ROOT; }; C28F7F2B73F9F0983878D44D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; C6641462093807C7726C77E6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; CD1BCA4BFD074C07318C65CC /* LiveLogsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLogsModel.swift; sourceTree = ""; }; @@ -183,7 +190,7 @@ 9F007AA80ECE5A10D372536D /* Packages */ = { isa = PBXGroup; children = ( - 6BF9453E787F70E21D026A1B /* security-review-candidate */, + BF28B9563BB119B8B18C547A /* IOSDeveloperToolkit-Swift */, ); name = Packages; sourceTree = ""; @@ -199,6 +206,26 @@ }; /* End PBXGroup section */ +/* Begin PBXLegacyTarget section */ + 32D9773338414C6CCAE8E3EB /* FirmwareHelpers */ = { + isa = PBXLegacyTarget; + buildArgumentsString = "\"$(SRCROOT)/scripts/build-restore-helpers.sh\""; + buildConfigurationList = 0B6AB51D049446E3B83F0923 /* Build configuration list for PBXLegacyTarget "FirmwareHelpers" */; + buildPhases = ( + A3D3DE802B2DBA3CABBBA244 /* Sources */, + ); + buildToolPath = /bin/bash; + buildWorkingDirectory = "$(SRCROOT)"; + dependencies = ( + ); + name = FirmwareHelpers; + packageProductDependencies = ( + ); + passBuildSettingsInEnvironment = 1; + productName = FirmwareHelpers; + }; +/* End PBXLegacyTarget section */ + /* Begin PBXNativeTarget section */ C94E9FC9726C7F28D33DAAA6 /* iOSDeveloperToolkitUITests */ = { isa = PBXNativeTarget; @@ -225,10 +252,12 @@ 488940A04D4B7D5484BF1BF5 /* Sources */, B142682F4800FD134D687DD7 /* Resources */, 31B9C83311C44A75CE1F24EC /* Frameworks */, + 50E45B1F44A134488677AB2E /* Embed Firmware Helpers */, ); buildRules = ( ); dependencies = ( + C445AAFBA3D83548EAF34E82 /* PBXTargetDependency */, ); name = iOSDeveloperToolkit; packageProductDependencies = ( @@ -277,6 +306,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( + 32D9773338414C6CCAE8E3EB /* FirmwareHelpers */, FE597393124482775C8D00A3 /* iOSDeveloperToolkit */, C94E9FC9726C7F28D33DAAA6 /* iOSDeveloperToolkitUITests */, ); @@ -294,6 +324,79 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 50E45B1F44A134488677AB2E /* Embed Firmware Helpers */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/scripts/embed-restore-helpers.sh", + "$(SRCROOT)/build-output/restore-helpers/out/bin/idevicerestore", + "$(SRCROOT)/build-output/restore-helpers/out/bin/irecovery", + "$(SRCROOT)/build-output/restore-helpers/out/SOURCES.txt", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/idevicerestore/AUTHORS", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/idevicerestore/COPYING", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/libimobiledevice-glue/COPYING", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/libimobiledevice/AUTHORS", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/libimobiledevice/COPYING", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/libimobiledevice/COPYING.LESSER", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/libirecovery/COPYING", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/libplist/AUTHORS", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/libplist/COPYING", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/libplist/COPYING.LESSER", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/libtatsu/COPYING", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/libusbmuxd/AUTHORS", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/libusbmuxd/COPYING", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/libzip/AUTHORS", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/libzip/LICENSE", + "$(SRCROOT)/build-output/restore-helpers/out/licenses/openssl/LICENSE.txt", + ); + name = "Embed Firmware Helpers"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Helpers", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Helpers/idevicerestore", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Helpers/irecovery", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/SOURCES.txt", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/idevicerestore", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libimobiledevice", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libimobiledevice-glue", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libirecovery", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libplist", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libtatsu", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libusbmuxd", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libzip", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/openssl", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/idevicerestore/AUTHORS", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/idevicerestore/COPYING", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libimobiledevice-glue/COPYING", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libimobiledevice/AUTHORS", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libimobiledevice/COPYING", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libimobiledevice/COPYING.LESSER", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libirecovery/COPYING", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libplist/AUTHORS", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libplist/COPYING", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libplist/COPYING.LESSER", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libtatsu/COPYING", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libusbmuxd/AUTHORS", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libusbmuxd/COPYING", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libzip/AUTHORS", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libzip/LICENSE", + "$(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/openssl/LICENSE.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/bash; + shellScript = "\"$SRCROOT/scripts/embed-restore-helpers.sh\""; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 488940A04D4B7D5484BF1BF5 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -334,6 +437,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A3D3DE802B2DBA3CABBBA244 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -342,6 +452,11 @@ target = FE597393124482775C8D00A3 /* iOSDeveloperToolkit */; targetProxy = 7D859C2B12ECAB79034F33D7 /* PBXContainerItemProxy */; }; + C445AAFBA3D83548EAF34E82 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 32D9773338414C6CCAE8E3EB /* FirmwareHelpers */; + targetProxy = 513BA446F86D4DC4FF41E600 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -392,6 +507,19 @@ }; name = Release; }; + 4491CC91B5B2673366D99C10 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.hideouts.FirmwareHelpers; + SDKROOT = macosx; + }; + name = Debug; + }; 4D0BEA2325533C972D0ED3C4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -418,6 +546,19 @@ }; name = Debug; }; + 6321CC0914E0448A350E60EE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.hideouts.FirmwareHelpers; + SDKROOT = macosx; + }; + name = Release; + }; 9E7571E39A44000D901788E0 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -569,6 +710,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 0B6AB51D049446E3B83F0923 /* Build configuration list for PBXLegacyTarget "FirmwareHelpers" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4491CC91B5B2673366D99C10 /* Debug */, + 6321CC0914E0448A350E60EE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; 6B061FE82399C983728CD096 /* Build configuration list for PBXProject "iOSDeveloperToolkit" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/project.yml b/project.yml index a5e963d..b4ada7c 100644 --- a/project.yml +++ b/project.yml @@ -21,18 +21,87 @@ packages: Toolkit: path: . targets: + FirmwareHelpers: + type: tool + platform: macOS + legacy: + toolPath: /bin/bash + arguments: '"$(SRCROOT)/scripts/build-restore-helpers.sh"' + passSettings: true + workingDirectory: $(SRCROOT) iOSDeveloperToolkit: type: application platform: macOS sources: - path: App/iOSDeveloperToolkit dependencies: + - target: FirmwareHelpers + link: false + embed: false - package: Toolkit product: ToolkitCore - package: Toolkit product: DeviceKit - package: Toolkit product: ToolkitFeatures + postBuildScripts: + - script: '"$SRCROOT/scripts/embed-restore-helpers.sh"' + name: Embed Firmware Helpers + shell: /bin/bash + showEnvVars: false + basedOnDependencyAnalysis: false + inputFiles: + - $(SRCROOT)/scripts/embed-restore-helpers.sh + - $(SRCROOT)/build-output/restore-helpers/out/bin/idevicerestore + - $(SRCROOT)/build-output/restore-helpers/out/bin/irecovery + - $(SRCROOT)/build-output/restore-helpers/out/SOURCES.txt + - $(SRCROOT)/build-output/restore-helpers/out/licenses/idevicerestore/AUTHORS + - $(SRCROOT)/build-output/restore-helpers/out/licenses/idevicerestore/COPYING + - $(SRCROOT)/build-output/restore-helpers/out/licenses/libimobiledevice-glue/COPYING + - $(SRCROOT)/build-output/restore-helpers/out/licenses/libimobiledevice/AUTHORS + - $(SRCROOT)/build-output/restore-helpers/out/licenses/libimobiledevice/COPYING + - $(SRCROOT)/build-output/restore-helpers/out/licenses/libimobiledevice/COPYING.LESSER + - $(SRCROOT)/build-output/restore-helpers/out/licenses/libirecovery/COPYING + - $(SRCROOT)/build-output/restore-helpers/out/licenses/libplist/AUTHORS + - $(SRCROOT)/build-output/restore-helpers/out/licenses/libplist/COPYING + - $(SRCROOT)/build-output/restore-helpers/out/licenses/libplist/COPYING.LESSER + - $(SRCROOT)/build-output/restore-helpers/out/licenses/libtatsu/COPYING + - $(SRCROOT)/build-output/restore-helpers/out/licenses/libusbmuxd/AUTHORS + - $(SRCROOT)/build-output/restore-helpers/out/licenses/libusbmuxd/COPYING + - $(SRCROOT)/build-output/restore-helpers/out/licenses/libzip/AUTHORS + - $(SRCROOT)/build-output/restore-helpers/out/licenses/libzip/LICENSE + - $(SRCROOT)/build-output/restore-helpers/out/licenses/openssl/LICENSE.txt + outputFiles: + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Helpers + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Helpers/idevicerestore + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Helpers/irecovery + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/SOURCES.txt + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/idevicerestore + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libimobiledevice + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libimobiledevice-glue + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libirecovery + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libplist + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libtatsu + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libusbmuxd + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libzip + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/openssl + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/idevicerestore/AUTHORS + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/idevicerestore/COPYING + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libimobiledevice-glue/COPYING + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libimobiledevice/AUTHORS + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libimobiledevice/COPYING + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libimobiledevice/COPYING.LESSER + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libirecovery/COPYING + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libplist/AUTHORS + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libplist/COPYING + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libplist/COPYING.LESSER + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libtatsu/COPYING + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libusbmuxd/AUTHORS + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libusbmuxd/COPYING + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libzip/AUTHORS + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/libzip/LICENSE + - $(TARGET_BUILD_DIR)/$(FULL_PRODUCT_NAME)/Contents/Resources/Licenses/restore-helpers/openssl/LICENSE.txt settings: base: PRODUCT_NAME: "iOS Developer Toolkit (Swift)" diff --git a/scripts/build-restore-helpers.sh b/scripts/build-restore-helpers.sh index e5da48c..3cd1d95 100755 --- a/scripts/build-restore-helpers.sh +++ b/scripts/build-restore-helpers.sh @@ -30,6 +30,31 @@ OUT="${1:-$WORK/out}" SRC="$WORK/src" MIN_MACOS=14.0 JOBS="$(sysctl -n hw.ncpu)" +STAMP="$(shasum -a 256 "$ROOT/scripts/build-restore-helpers.sh" | cut -d' ' -f1)" +STATE="$WORK/cache-$STAMP" + +for homebrew_bin in /opt/homebrew/bin /usr/local/bin; do + if [[ -d "$homebrew_bin" ]]; then PATH="$homebrew_bin:$PATH"; fi +done +export PATH + +mkdir -p "$WORK" +LOCK="$WORK/.build-lock" +if ! mkdir "$LOCK" 2>/dev/null; then + [[ -d "$LOCK" ]] || fail "cannot create build lock $LOCK" + owner="unknown" + if [[ -f "$LOCK/pid" ]]; then read -r owner < "$LOCK/pid"; fi + fail "another helper build owns $LOCK (PID $owner). Check that process before removing a stale lock." +fi +printf '%s\n' "$$" > "$LOCK/pid" +trap 'rm -f "$LOCK/pid"; rmdir "$LOCK"' EXIT + +if [[ -f "$OUT/STAMP" && "$(cat "$OUT/STAMP")" == "$STAMP" \ + && -x "$OUT/bin/idevicerestore" && -x "$OUT/bin/irecovery" \ + && -f "$OUT/SOURCES.txt" && -d "$OUT/licenses" ]]; then + step "Reusing firmware helpers: $OUT" + exit 0 +fi # name|git URL|commit|version label GIT_SOURCES=( @@ -46,11 +71,37 @@ OPENSSL_VERSION=3.5.8 OPENSSL_URL="https://github.com/openssl/openssl/releases/download/openssl-$OPENSSL_VERSION/openssl-$OPENSSL_VERSION.tar.gz" OPENSSL_SHA256=a8f84a39918ec6415ce765d9b429d313ba97b8143169c172e734b9514464f5b2 -for tool in autoconf automake glibtoolize pkg-config cmake git xcrun lipo; do +for tool in autoconf autom4te automake glibtoolize pkg-config cmake git xcrun lipo; do command -v "$tool" >/dev/null || fail "$tool is missing (brew install autoconf automake libtool pkg-config cmake)" done SDK="$(xcrun --sdk macosx --show-sdk-path)" +# Autoconf 2.73's Perl exec handoff stalls on this macOS host. A local copy uses +# a child process and preserves its exit status; installed tools and source pins are unchanged. +if [[ "$(autoconf --version | head -1)" == "autoconf (GNU Autoconf) 2.73" ]]; then + step "Preparing the build-local Autoconf 2.73 process-launch compatibility patch" + mkdir -p "$STATE/tools" + /usr/bin/perl -0777 -e ' + my $source = <>; + my $original = q{exec {$autom4te_command[0]} @autom4te_command;}; + my $replacement = q{my $status = system {$autom4te_command[0]} @autom4te_command; +exit ($status == -1 ? 127 : $status & 127 ? 128 + ($status & 127) : $status >> 8);}; + my $count = $source =~ s/\Q$original\E/$replacement/g; + die "Unsupported Autoconf 2.73 launch contract\n" unless $count == 1; + print $source; + ' "$(command -v autoconf)" > "$STATE/tools/autoconf.pl" + cat > "$STATE/tools/autoconf" <<'EOF' +#!/bin/bash +set -euo pipefail +/usr/bin/perl "$(dirname "$0")/autoconf.pl" "$@" +EOF + chmod +x "$STATE/tools/autoconf" + printf -v AUTOCONF '%q' "$STATE/tools/autoconf" + export AUTOCONF + PATH="$STATE/tools:$PATH" + export PATH +fi + fetch_sources() { mkdir -p "$SRC" for entry in "${GIT_SOURCES[@]}"; do @@ -77,7 +128,7 @@ fetch_sources() { # autotools project: name, extra configure flags… build_autotools() { local arch="$1" name="$2"; shift 2 - local prefix="$WORK/prefix-$arch" build="$WORK/build-$arch/$name" + local prefix="$STATE/prefix-$arch" build="$STATE/build-$arch/$name" [[ -f "$build/.done" ]] && return 0 step "[$arch] $name" rm -rf "$build"; mkdir -p "$build" @@ -98,7 +149,7 @@ build_autotools() { } build_openssl() { - local arch="$1" prefix="$WORK/prefix-$arch" build="$WORK/build-$arch/openssl" + local arch="$1" prefix="$STATE/prefix-$arch" build="$STATE/build-$arch/openssl" [[ -f "$build/.done" ]] && return 0 step "[$arch] OpenSSL $OPENSSL_VERSION" rm -rf "$build"; mkdir -p "$build" @@ -114,7 +165,7 @@ build_openssl() { } build_libzip() { - local arch="$1" prefix="$WORK/prefix-$arch" build="$WORK/build-$arch/libzip" + local arch="$1" prefix="$STATE/prefix-$arch" build="$STATE/build-$arch/libzip" [[ -f "$build/.done" ]] && return 0 step "[$arch] libzip" rm -rf "$build"; mkdir -p "$build" @@ -131,7 +182,7 @@ build_libzip() { # pkg-config files for the libraries macOS provides (curl and zlib from the SDK). system_pkgconfig() { - local prefix="$WORK/prefix-$1" + local prefix="$STATE/prefix-$1" mkdir -p "$prefix/lib/pkgconfig" cat > "$prefix/lib/pkgconfig/libcurl.pc" <> "$OUT/SOURCES.txt" -shasum -a 256 "$ROOT/scripts/build-restore-helpers.sh" | cut -d' ' -f1 > "$OUT/STAMP" +printf '%s\n' "$STAMP" > "$OUT/STAMP" step "Done: $OUT" ls -l "$OUT/bin" diff --git a/scripts/embed-restore-helpers.sh b/scripts/embed-restore-helpers.sh new file mode 100755 index 0000000..de07c09 --- /dev/null +++ b/scripts/embed-restore-helpers.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +HELPERS="$ROOT/build-output/restore-helpers/out" +STAGING="$(mktemp -d "$TEMP_DIR/firmware-helpers.XXXXXX")" +trap 'rm -rf "$STAGING"' EXIT + +DESTINATION="$TARGET_BUILD_DIR/$FULL_PRODUCT_NAME/Contents/Helpers" +mkdir -p "$DESTINATION" +for helper in idevicerestore irecovery; do + SOURCE="$HELPERS/bin/$helper" + [[ -x "$SOURCE" ]] || { echo "Missing executable firmware helper: $SOURCE. Build the FirmwareHelpers target first." >&2; exit 1; } + # install and codesign create temporary files. Use Xcode's writable staging + # directory, then copy only the declared final output into the app bundle. + install -m 755 "$SOURCE" "$STAGING/$helper" + if [[ "$CODE_SIGNING_ALLOWED" == YES ]]; then + codesign --force --sign "$CODE_SIGN_IDENTITY" --options runtime --timestamp=none \ + --identifier "io.hideouts.iOSDeveloperToolkit.$helper" "$STAGING/$helper" + fi + cp "$STAGING/$helper" "$DESTINATION/$helper" + chmod 755 "$DESTINATION/$helper" +done + +LICENSE_DESTINATION="$TARGET_BUILD_DIR/$FULL_PRODUCT_NAME/Contents/Resources/Licenses/restore-helpers" +mkdir -p "$LICENSE_DESTINATION" +cp -R "$HELPERS/licenses/." "$LICENSE_DESTINATION/" +cp "$HELPERS/SOURCES.txt" "$LICENSE_DESTINATION/SOURCES.txt" From f5f0b5efe81332ea33bfbd087e2a4dd344474447 Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:53:20 -0700 Subject: [PATCH 2/3] Avoid Swift 6.3 crash in command category binding --- App/iOSDeveloperToolkit/Views/ActionsViews.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/iOSDeveloperToolkit/Views/ActionsViews.swift b/App/iOSDeveloperToolkit/Views/ActionsViews.swift index 47d5e89..61581c9 100644 --- a/App/iOSDeveloperToolkit/Views/ActionsViews.swift +++ b/App/iOSDeveloperToolkit/Views/ActionsViews.swift @@ -101,7 +101,7 @@ struct ActionsView: View { TextField("Search guided commands", text: $search) .textFieldStyle(.roundedBorder) .accessibilityIdentifier("command-search") - Picker("Category", selection: Binding(get: { filter }, set: selectFilter)) { + Picker("Category", selection: Binding(get: { filter }, set: { selectFilter($0) })) { Text("All categories").tag(GuidedCommandFilter.all) ForEach(GuidedCommandCategory.allCases) { category in Text(category.rawValue) From cf9ac8e6ae5e51a083875fa3a8edbfdccb705cdd Mon Sep 17 00:00:00 2001 From: hideouts-io <83608068+hideouts-io@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:34:36 -0700 Subject: [PATCH 3/3] Keep command picker identity stable during native UI navigation --- App/UITests/SmokeUITests.swift | 7 +++++-- App/iOSDeveloperToolkit/Views/ActionsViews.swift | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/App/UITests/SmokeUITests.swift b/App/UITests/SmokeUITests.swift index 9051cff..89d406e 100644 --- a/App/UITests/SmokeUITests.swift +++ b/App/UITests/SmokeUITests.swift @@ -39,8 +39,11 @@ final class SmokeUITests: XCTestCase { XCTFail("Missing workspace sidebar") return } - guard sidebar.isHittable else { - XCTFail("The app sidebar is obscured. Make the test app window visible before running native UI tests.") + // Native outline containers can report unhittable while their controls are + // reachable. Check a visible sidebar button before scrolling, then the target row. + guard app.buttons["action-palette"].isHittable else { + attachScreenshot("unreachable-sidebar-\(identifier)") + XCTFail("The sidebar Action Palette button is not reachable. Make the test app window visible before running native UI tests.") return } for _ in 0..<8 { diff --git a/App/iOSDeveloperToolkit/Views/ActionsViews.swift b/App/iOSDeveloperToolkit/Views/ActionsViews.swift index 61581c9..a87278c 100644 --- a/App/iOSDeveloperToolkit/Views/ActionsViews.swift +++ b/App/iOSDeveloperToolkit/Views/ActionsViews.swift @@ -113,6 +113,8 @@ struct ActionsView: View { } } .labelsHidden() + // Separate the control's identity from its menu items' identifiers. + .accessibilityElement(children: .contain) .accessibilityIdentifier("command-categories") List(selection: $model.selectedActionID) { ForEach(GuidedCommandCategory.allCases.filter { category in actions.contains { category.contains($0) } }) { category in