diff --git a/.github/workflows/build-identity.yml b/.github/workflows/build-identity.yml new file mode 100644 index 0000000..bcbefaa --- /dev/null +++ b/.github/workflows/build-identity.yml @@ -0,0 +1,15 @@ +name: Build identity +on: + pull_request: + push: + branches: [main] +permissions: + contents: read +jobs: + identity: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: python3 scripts/test-build-identity.py + - run: ruby scripts/test-fastlane-identity.rb + - run: python3 scripts/test-archive-command.py diff --git a/.gitignore b/.gitignore index 2c993ed..28731c8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ /privacycommand/privacycommand.xcodeproj/project.xcworkspace/xcuserdata *.xcuserdatad /privacycommand/privacycommand.xcodeproj/project.xcworkspace + +# Local Archive outputs generated by the shared archive command. +/build/ diff --git a/Config/ArchiveTargets.json b/Config/ArchiveTargets.json new file mode 100644 index 0000000..f1ac8c5 --- /dev/null +++ b/Config/ArchiveTargets.json @@ -0,0 +1,11 @@ +{ + "project": "privacycommand/privacycommand.xcodeproj", + "spec": null, + "defaultPlatform": "macos", + "platforms": { + "macos": { + "scheme": "privacycommand", + "destination": "generic/platform=macOS" + } + } +} diff --git a/Config/Shared.xcconfig b/Config/Shared.xcconfig new file mode 100644 index 0000000..53fea07 --- /dev/null +++ b/Config/Shared.xcconfig @@ -0,0 +1,13 @@ +// Shared marketing version and generated build identity for every shipping target. +MARKETING_VERSION = 0.1.5 +CURRENT_PROJECT_VERSION = $(BUILD_NUMBER) +BUILD_NUMBER = 0 +BUILD_SHA = +BUILD_BRANCH = +BUILD_DIRTY = NO +BUILD_DIRTY_FILES = 0 +BUILD_DIFF_ID = +BUILD_DESCRIBE = +BUILD_TAGGED = NO +BUILD_TIMESTAMP = +BUILD_CHANNEL = xcode diff --git a/docs/ARCHIVING.md b/docs/ARCHIVING.md new file mode 100644 index 0000000..435d446 --- /dev/null +++ b/docs/ARCHIVING.md @@ -0,0 +1,26 @@ +# Archive and build identity + +The portfolio uses `YYYY.MMDD.HHMM` in UTC for build numbers. The marketing +version lives in `Config/Shared.xcconfig`; repository provenance is separate. +See the [canonical agreement](https://github.com/AdamXweb/AdamXweb/blob/main/docs/VERSIONING.md). + +Open the shared app scheme, select a device or Any Mac, then choose +**Product → Archive**. Each build captures a fresh identity automatically; +the app and its extensions share it. Generated projects must be regenerated +after pulling project changes. + +`just archive` generates the project when needed, archives, and checks the +Organizer, app and extension versions. Supported platforms are listed in +`Config/ArchiveTargets.json`; pass one with `just archive PLATFORM`. +For compile and metadata verification without certificates, run +`python3 scripts/archive-app.py --unsigned`. Add `--plan` to inspect commands. + +Normal archives use the project's signing setup and your Apple account. +Set `APPLE_TEAM_ID` when a team override is needed. Export and upload remain +separate. Release archives omit repository identifiers. Marketing versions +are unchanged by archiving. + +The format has minute precision: wait for the next UTC minute before +uploading a second build of the same version. An existing App Store version +with a larger legacy integer build may need a marketing version bump before +the first timestamp build can be uploaded. diff --git a/justfile b/justfile index 1e55a1b..fcc633b 100644 --- a/justfile +++ b/justfile @@ -27,3 +27,7 @@ release-local: [group("dev")] clean: rm -rf privacycommand/.build + +# Regenerate the project, Archive, and verify its app/extension build identity. +archive platform="macos": + python3 scripts/archive-app.py --platform {{quote(platform)}} diff --git a/privacycommand/privacycommand.xcodeproj/project.pbxproj b/privacycommand/privacycommand.xcodeproj/project.pbxproj index f3717ec..e257914 100644 --- a/privacycommand/privacycommand.xcodeproj/project.pbxproj +++ b/privacycommand/privacycommand.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 0BBBBBBB000000000000A006 /* UpdateFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A006 /* UpdateFetcher.swift */; }; 0BBBBBBB000000000000A007 /* WatchModeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A007 /* WatchModeManager.swift */; }; 0BBBBBBB000000000000A008 /* UpdateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A008 /* UpdateController.swift */; }; + 0BBBBBBB000000000000A009 /* BatchScanModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A009 /* BatchScanModel.swift */; }; 0BBBBBBB000000000000A101 /* DropTargetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A101 /* DropTargetView.swift */; }; 0BBBBBBB000000000000A102 /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A102 /* DashboardView.swift */; }; 0BBBBBBB000000000000A103 /* StaticAnalysisView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A103 /* StaticAnalysisView.swift */; }; @@ -63,6 +64,9 @@ 0BBBBBBB000000000000A12E /* TelemetrySummaryCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A12E /* TelemetrySummaryCard.swift */; }; 0BBBBBBB000000000000A12F /* PrivacyLabelsCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A12F /* PrivacyLabelsCard.swift */; }; 0BBBBBBB000000000000A130 /* UpdatesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A130 /* UpdatesSettingsView.swift */; }; + 0BBBBBBB000000000000A131 /* BatchScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A131 /* BatchScanView.swift */; }; + 0BBBBBBB000000000000A132 /* PermissionMatrixView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A132 /* PermissionMatrixView.swift */; }; + 0BBBBBBB000000000000A133 /* DecompiledClassesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A133 /* DecompiledClassesView.swift */; }; 0BBBBBBB000000000000A201 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A201 /* OnboardingView.swift */; }; 0BBBBBBB000000000000A202 /* OnboardingSteps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A202 /* OnboardingSteps.swift */; }; 0BBBBBBB000000000000A301 /* PDFExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A301 /* PDFExporter.swift */; }; @@ -74,6 +78,9 @@ 0BBBBBBB000000000000C006 /* OpenResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C006 /* OpenResource.swift */; }; 0BBBBBBB000000000000C007 /* LiveProbeEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C007 /* LiveProbeEvent.swift */; }; 0BBBBBBB000000000000C008 /* AppStoreInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C008 /* AppStoreInfo.swift */; }; + 0BBBBBBB000000000000C009 /* TCCGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C009 /* TCCGrant.swift */; }; + 0BBBBBBB000000000000C00A /* PermissionReconciliation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C00A /* PermissionReconciliation.swift */; }; + 0BBBBBBB000000000000C00B /* DecompilationIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C00B /* DecompilationIndex.swift */; }; 0BBBBBBB000000000000C101 /* MachOInspector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C101 /* MachOInspector.swift */; }; 0BBBBBBB000000000000C102 /* CodesignWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C102 /* CodesignWrapper.swift */; }; 0BBBBBBB000000000000C103 /* EntitlementsReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C103 /* EntitlementsReader.swift */; }; @@ -86,9 +93,6 @@ 0BBBBBBB000000000000C10A /* UpdateMechanismDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C10A /* UpdateMechanismDetector.swift */; }; 0BBBBBBB000000000000C10B /* AppcastParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C10B /* AppcastParser.swift */; }; 0BBBBBBB000000000000C10C /* DisassemblyAnalyzer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C10C /* DisassemblyAnalyzer.swift */; }; - 0BBBBBBB000000000000C120 /* BinaryCapabilityAnalyzer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C120 /* BinaryCapabilityAnalyzer.swift */; }; - 0BBBBBBB000000000000C11E /* GhidraDecompiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C11E /* GhidraDecompiler.swift */; }; - 0BBBBBBB000000000000C11F /* ConnectionTracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C11F /* ConnectionTracer.swift */; }; 0BBBBBBB000000000000C10D /* SDKFingerprintDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C10D /* SDKFingerprintDatabase.swift */; }; 0BBBBBBB000000000000C10E /* SDKFingerprintDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C10E /* SDKFingerprintDetector.swift */; }; 0BBBBBBB000000000000C10F /* SecretsScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C10F /* SecretsScanner.swift */; }; @@ -98,31 +102,30 @@ 0BBBBBBB000000000000C113 /* EmbeddedAssetScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C113 /* EmbeddedAssetScanner.swift */; }; 0BBBBBBB000000000000C114 /* BehaviorAnalyzer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C114 /* BehaviorAnalyzer.swift */; }; 0BBBBBBB000000000000C115 /* PrivacyManifestReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C115 /* PrivacyManifestReader.swift */; }; - 0BBBBBBB000000000000C153 /* RequiredReasonAPIs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C153 /* RequiredReasonAPIs.swift */; }; - 0BBBBBBB000000000000C152 /* AppExtensionScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C152 /* AppExtensionScanner.swift */; }; - 0BBBBBBB000000000000C151 /* AppRuntimeDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C151 /* AppRuntimeDetector.swift */; }; - 0BBBBBBB000000000000C150 /* EmbeddedResourceScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C150 /* EmbeddedResourceScanner.swift */; }; 0BBBBBBB000000000000C116 /* SandboxContainerInspector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C116 /* SandboxContainerInspector.swift */; }; 0BBBBBBB000000000000C117 /* BTMAuditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C117 /* BTMAuditor.swift */; }; - 0BBBBBBB000000000000C125 /* MalimiteImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C125 /* MalimiteImporter.swift */; }; - 0BBBBBBB000000000000C126 /* MacAptImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C126 /* MacAptImporter.swift */; }; - 0BBBBBBB000000000000C702 /* ToolCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C702 /* ToolCatalog.swift */; }; - 0BBBBBBB000000000000C124 /* GhidraProgramDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C124 /* GhidraProgramDump.swift */; }; - 0BBBBBBB000000000000C00B /* DecompilationIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C00B /* DecompilationIndex.swift */; }; - 0BBBBBBB000000000000C602 /* DecompilationIndexStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C602 /* DecompilationIndexStore.swift */; }; - 0BBBBBBB000000000000A133 /* DecompiledClassesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A133 /* DecompiledClassesView.swift */; }; - 0BBBBBBB000000000000C121 /* SQLiteReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C121 /* SQLiteReader.swift */; }; - 0BBBBBBB000000000000C122 /* TCCAuditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C122 /* TCCAuditor.swift */; }; - 0BBBBBBB000000000000C123 /* PermissionReconciler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C123 /* PermissionReconciler.swift */; }; - 0BBBBBBB000000000000C009 /* TCCGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C009 /* TCCGrant.swift */; }; - 0BBBBBBB000000000000C00A /* PermissionReconciliation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C00A /* PermissionReconciliation.swift */; }; - 0BBBBBBB000000000000A132 /* PermissionMatrixView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A132 /* PermissionMatrixView.swift */; }; 0BBBBBBB000000000000C118 /* NotarizationDeepDive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C118 /* NotarizationDeepDive.swift */; }; 0BBBBBBB000000000000C119 /* DMGMounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C119 /* DMGMounter.swift */; }; 0BBBBBBB000000000000C11A /* FlagsScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C11A /* FlagsScanner.swift */; }; 0BBBBBBB000000000000C11B /* MASReceiptDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C11B /* MASReceiptDetector.swift */; }; 0BBBBBBB000000000000C11C /* AppStoreLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C11C /* AppStoreLookup.swift */; }; 0BBBBBBB000000000000C11D /* AppStorePrivacyLabelFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C11D /* AppStorePrivacyLabelFetcher.swift */; }; + 0BBBBBBB000000000000C11E /* GhidraDecompiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C11E /* GhidraDecompiler.swift */; }; + 0BBBBBBB000000000000C11F /* ConnectionTracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C11F /* ConnectionTracer.swift */; }; + 0BBBBBBB000000000000C120 /* BinaryCapabilityAnalyzer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C120 /* BinaryCapabilityAnalyzer.swift */; }; + 0BBBBBBB000000000000C121 /* SQLiteReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C121 /* SQLiteReader.swift */; }; + 0BBBBBBB000000000000C122 /* TCCAuditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C122 /* TCCAuditor.swift */; }; + 0BBBBBBB000000000000C123 /* PermissionReconciler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C123 /* PermissionReconciler.swift */; }; + 0BBBBBBB000000000000C124 /* GhidraProgramDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C124 /* GhidraProgramDump.swift */; }; + 0BBBBBBB000000000000C125 /* MalimiteImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C125 /* MalimiteImporter.swift */; }; + 0BBBBBBB000000000000C126 /* MacAptImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C126 /* MacAptImporter.swift */; }; + 0BBBBBBB000000000000C141 /* InstalledAppEnumerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C141 /* InstalledAppEnumerator.swift */; }; + 0BBBBBBB000000000000C142 /* BatchAppResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C142 /* BatchAppResult.swift */; }; + 0BBBBBBB000000000000C143 /* BatchAnalyzer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C143 /* BatchAnalyzer.swift */; }; + 0BBBBBBB000000000000C150 /* EmbeddedResourceScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C150 /* EmbeddedResourceScanner.swift */; }; + 0BBBBBBB000000000000C151 /* AppRuntimeDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C151 /* AppRuntimeDetector.swift */; }; + 0BBBBBBB000000000000C152 /* AppExtensionScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C152 /* AppExtensionScanner.swift */; }; + 0BBBBBBB000000000000C153 /* RequiredReasonAPIs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C153 /* RequiredReasonAPIs.swift */; }; 0BBBBBBB000000000000C201 /* ProcessRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C201 /* ProcessRunner.swift */; }; 0BBBBBBB000000000000C202 /* ProcessTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C202 /* ProcessTracker.swift */; }; 0BBBBBBB000000000000C203 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C203 /* NetworkMonitor.swift */; }; @@ -138,22 +141,19 @@ 0BBBBBBB000000000000C301 /* PathClassifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C301 /* PathClassifier.swift */; }; 0BBBBBBB000000000000C302 /* RiskClassifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C302 /* RiskClassifier.swift */; }; 0BBBBBBB000000000000C303 /* RiskScorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C303 /* RiskScorer.swift */; }; - 0BBBBBBB000000000000C141 /* InstalledAppEnumerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C141 /* InstalledAppEnumerator.swift */; }; - 0BBBBBBB000000000000C142 /* BatchAppResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C142 /* BatchAppResult.swift */; }; - 0BBBBBBB000000000000C143 /* BatchAnalyzer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C143 /* BatchAnalyzer.swift */; }; - 0BBBBBBB000000000000A009 /* BatchScanModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A009 /* BatchScanModel.swift */; }; - 0BBBBBBB000000000000A131 /* BatchScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000A131 /* BatchScanView.swift */; }; 0BBBBBBB000000000000C304 /* DomainClassifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C304 /* DomainClassifier.swift */; }; + 0BBBBBBB000000000000C305 /* ContributorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C305 /* ContributorSource.swift */; }; 0BBBBBBB000000000000C306 /* KnownTLDs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C306 /* KnownTLDs.swift */; }; 0BBBBBBB000000000000C307 /* DomainValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C307 /* DomainValidator.swift */; }; - 0BBBBBBB000000000000C305 /* ContributorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C305 /* ContributorSource.swift */; }; 0BBBBBBB000000000000C401 /* JSONExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C401 /* JSONExporter.swift */; }; 0BBBBBBB000000000000C402 /* HTMLExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C402 /* HTMLExporter.swift */; }; 0BBBBBBB000000000000C403 /* ReportDiffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C403 /* ReportDiffer.swift */; }; 0BBBBBBB000000000000C501 /* HelperToolProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C501 /* HelperToolProtocol.swift */; }; 0BBBBBBB000000000000C502 /* HelperToolProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C501 /* HelperToolProtocol.swift */; }; 0BBBBBBB000000000000C601 /* RunStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C601 /* RunStore.swift */; }; + 0BBBBBBB000000000000C602 /* DecompilationIndexStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C602 /* DecompilationIndexStore.swift */; }; 0BBBBBBB000000000000C701 /* KnowledgeBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C701 /* KnowledgeBase.swift */; }; + 0BBBBBBB000000000000C702 /* ToolCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C702 /* ToolCatalog.swift */; }; 0BBBBBBB000000000000C801 /* RunSearcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C801 /* RunSearcher.swift */; }; 0BBBBBBB000000000000C901 /* GuestProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C901 /* GuestProtocol.swift */; }; 0BBBBBBB000000000000C902 /* UpdateChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFFFFFF000000000000C902 /* UpdateChannel.swift */; }; @@ -237,6 +237,7 @@ 0FFFFFFF000000000000A006 /* UpdateFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateFetcher.swift; sourceTree = ""; }; 0FFFFFFF000000000000A007 /* WatchModeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchModeManager.swift; sourceTree = ""; }; 0FFFFFFF000000000000A008 /* UpdateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateController.swift; sourceTree = ""; }; + 0FFFFFFF000000000000A009 /* BatchScanModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchScanModel.swift; sourceTree = ""; }; 0FFFFFFF000000000000A101 /* DropTargetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropTargetView.swift; sourceTree = ""; }; 0FFFFFFF000000000000A102 /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = ""; }; 0FFFFFFF000000000000A103 /* StaticAnalysisView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticAnalysisView.swift; sourceTree = ""; }; @@ -285,6 +286,9 @@ 0FFFFFFF000000000000A12E /* TelemetrySummaryCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetrySummaryCard.swift; sourceTree = ""; }; 0FFFFFFF000000000000A12F /* PrivacyLabelsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyLabelsCard.swift; sourceTree = ""; }; 0FFFFFFF000000000000A130 /* UpdatesSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatesSettingsView.swift; sourceTree = ""; }; + 0FFFFFFF000000000000A131 /* BatchScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchScanView.swift; sourceTree = ""; }; + 0FFFFFFF000000000000A132 /* PermissionMatrixView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionMatrixView.swift; sourceTree = ""; }; + 0FFFFFFF000000000000A133 /* DecompiledClassesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecompiledClassesView.swift; sourceTree = ""; }; 0FFFFFFF000000000000A201 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; 0FFFFFFF000000000000A202 /* OnboardingSteps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSteps.swift; sourceTree = ""; }; 0FFFFFFF000000000000A301 /* PDFExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFExporter.swift; sourceTree = ""; }; @@ -308,6 +312,9 @@ 0FFFFFFF000000000000C006 /* OpenResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenResource.swift; sourceTree = ""; }; 0FFFFFFF000000000000C007 /* LiveProbeEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveProbeEvent.swift; sourceTree = ""; }; 0FFFFFFF000000000000C008 /* AppStoreInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreInfo.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C009 /* TCCGrant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCCGrant.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C00A /* PermissionReconciliation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionReconciliation.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C00B /* DecompilationIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecompilationIndex.swift; sourceTree = ""; }; 0FFFFFFF000000000000C101 /* MachOInspector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MachOInspector.swift; sourceTree = ""; }; 0FFFFFFF000000000000C102 /* CodesignWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodesignWrapper.swift; sourceTree = ""; }; 0FFFFFFF000000000000C103 /* EntitlementsReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntitlementsReader.swift; sourceTree = ""; }; @@ -320,9 +327,6 @@ 0FFFFFFF000000000000C10A /* UpdateMechanismDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateMechanismDetector.swift; sourceTree = ""; }; 0FFFFFFF000000000000C10B /* AppcastParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppcastParser.swift; sourceTree = ""; }; 0FFFFFFF000000000000C10C /* DisassemblyAnalyzer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisassemblyAnalyzer.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C120 /* BinaryCapabilityAnalyzer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryCapabilityAnalyzer.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C11E /* GhidraDecompiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhidraDecompiler.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C11F /* ConnectionTracer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionTracer.swift; sourceTree = ""; }; 0FFFFFFF000000000000C10D /* SDKFingerprintDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKFingerprintDatabase.swift; sourceTree = ""; }; 0FFFFFFF000000000000C10E /* SDKFingerprintDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKFingerprintDetector.swift; sourceTree = ""; }; 0FFFFFFF000000000000C10F /* SecretsScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretsScanner.swift; sourceTree = ""; }; @@ -332,31 +336,30 @@ 0FFFFFFF000000000000C113 /* EmbeddedAssetScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedAssetScanner.swift; sourceTree = ""; }; 0FFFFFFF000000000000C114 /* BehaviorAnalyzer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorAnalyzer.swift; sourceTree = ""; }; 0FFFFFFF000000000000C115 /* PrivacyManifestReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyManifestReader.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C153 /* RequiredReasonAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredReasonAPIs.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C152 /* AppExtensionScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExtensionScanner.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C151 /* AppRuntimeDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRuntimeDetector.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C150 /* EmbeddedResourceScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedResourceScanner.swift; sourceTree = ""; }; 0FFFFFFF000000000000C116 /* SandboxContainerInspector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxContainerInspector.swift; sourceTree = ""; }; 0FFFFFFF000000000000C117 /* BTMAuditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTMAuditor.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C125 /* MalimiteImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MalimiteImporter.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C126 /* MacAptImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAptImporter.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C702 /* ToolCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolCatalog.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C124 /* GhidraProgramDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhidraProgramDump.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C00B /* DecompilationIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecompilationIndex.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C602 /* DecompilationIndexStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecompilationIndexStore.swift; sourceTree = ""; }; - 0FFFFFFF000000000000A133 /* DecompiledClassesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecompiledClassesView.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C121 /* SQLiteReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLiteReader.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C122 /* TCCAuditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCCAuditor.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C123 /* PermissionReconciler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionReconciler.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C009 /* TCCGrant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCCGrant.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C00A /* PermissionReconciliation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionReconciliation.swift; sourceTree = ""; }; - 0FFFFFFF000000000000A132 /* PermissionMatrixView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionMatrixView.swift; sourceTree = ""; }; 0FFFFFFF000000000000C118 /* NotarizationDeepDive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotarizationDeepDive.swift; sourceTree = ""; }; 0FFFFFFF000000000000C119 /* DMGMounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMGMounter.swift; sourceTree = ""; }; 0FFFFFFF000000000000C11A /* FlagsScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagsScanner.swift; sourceTree = ""; }; 0FFFFFFF000000000000C11B /* MASReceiptDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MASReceiptDetector.swift; sourceTree = ""; }; 0FFFFFFF000000000000C11C /* AppStoreLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreLookup.swift; sourceTree = ""; }; 0FFFFFFF000000000000C11D /* AppStorePrivacyLabelFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorePrivacyLabelFetcher.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C11E /* GhidraDecompiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhidraDecompiler.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C11F /* ConnectionTracer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionTracer.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C120 /* BinaryCapabilityAnalyzer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryCapabilityAnalyzer.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C121 /* SQLiteReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLiteReader.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C122 /* TCCAuditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCCAuditor.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C123 /* PermissionReconciler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionReconciler.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C124 /* GhidraProgramDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhidraProgramDump.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C125 /* MalimiteImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MalimiteImporter.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C126 /* MacAptImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAptImporter.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C141 /* InstalledAppEnumerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledAppEnumerator.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C142 /* BatchAppResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchAppResult.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C143 /* BatchAnalyzer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchAnalyzer.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C150 /* EmbeddedResourceScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedResourceScanner.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C151 /* AppRuntimeDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRuntimeDetector.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C152 /* AppExtensionScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExtensionScanner.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C153 /* RequiredReasonAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredReasonAPIs.swift; sourceTree = ""; }; 0FFFFFFF000000000000C201 /* ProcessRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessRunner.swift; sourceTree = ""; }; 0FFFFFFF000000000000C202 /* ProcessTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessTracker.swift; sourceTree = ""; }; 0FFFFFFF000000000000C203 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; @@ -372,21 +375,18 @@ 0FFFFFFF000000000000C301 /* PathClassifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathClassifier.swift; sourceTree = ""; }; 0FFFFFFF000000000000C302 /* RiskClassifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiskClassifier.swift; sourceTree = ""; }; 0FFFFFFF000000000000C303 /* RiskScorer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiskScorer.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C141 /* InstalledAppEnumerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledAppEnumerator.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C142 /* BatchAppResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchAppResult.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C143 /* BatchAnalyzer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchAnalyzer.swift; sourceTree = ""; }; - 0FFFFFFF000000000000A009 /* BatchScanModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchScanModel.swift; sourceTree = ""; }; - 0FFFFFFF000000000000A131 /* BatchScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchScanView.swift; sourceTree = ""; }; 0FFFFFFF000000000000C304 /* DomainClassifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainClassifier.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C305 /* ContributorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributorSource.swift; sourceTree = ""; }; 0FFFFFFF000000000000C306 /* KnownTLDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnownTLDs.swift; sourceTree = ""; }; 0FFFFFFF000000000000C307 /* DomainValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainValidator.swift; sourceTree = ""; }; - 0FFFFFFF000000000000C305 /* ContributorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributorSource.swift; sourceTree = ""; }; 0FFFFFFF000000000000C401 /* JSONExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONExporter.swift; sourceTree = ""; }; 0FFFFFFF000000000000C402 /* HTMLExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLExporter.swift; sourceTree = ""; }; 0FFFFFFF000000000000C403 /* ReportDiffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportDiffer.swift; sourceTree = ""; }; 0FFFFFFF000000000000C501 /* HelperToolProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperToolProtocol.swift; sourceTree = ""; }; 0FFFFFFF000000000000C601 /* RunStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunStore.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C602 /* DecompilationIndexStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecompilationIndexStore.swift; sourceTree = ""; }; 0FFFFFFF000000000000C701 /* KnowledgeBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnowledgeBase.swift; sourceTree = ""; }; + 0FFFFFFF000000000000C702 /* ToolCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolCatalog.swift; sourceTree = ""; }; 0FFFFFFF000000000000C801 /* RunSearcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunSearcher.swift; sourceTree = ""; }; 0FFFFFFF000000000000C901 /* GuestProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuestProtocol.swift; sourceTree = ""; }; 0FFFFFFF000000000000C902 /* UpdateChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateChannel.swift; sourceTree = ""; }; @@ -399,11 +399,22 @@ 0FFFFFFF000000000000D005 /* SecretsScannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretsScannerTests.swift; sourceTree = ""; }; 0FFFFFFF000000000000E001 /* privacycommand.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = privacycommand.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0FFFFFFF000000000000E002 /* privacycommandTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = privacycommandTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 45C386EC5EE885DE4E4DFA6F /* Shared.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Shared.xcconfig; path = ../Config/Shared.xcconfig; sourceTree = ""; }; A954CFEA2F9E0506005AC054 /* privacycommandHelper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = privacycommandHelper; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - A954CFEB2F9E0506005AC054 /* privacycommandHelper */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = privacycommandHelper; sourceTree = ""; }; + A954CFEB2F9E0506005AC054 /* privacycommandHelper */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = privacycommandHelper; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -444,6 +455,7 @@ 0CCCCCCC0000000000000D00 /* Tests */, A954CFEB2F9E0506005AC054 /* privacycommandHelper */, 0CCCCCCC0000000000000E00 /* Products */, + 45C386EC5EE885DE4E4DFA6F /* Shared.xcconfig */, ); sourceTree = ""; }; @@ -565,16 +577,6 @@ path = Resources; sourceTree = ""; }; - 0CCCCCCC0000000000000C14 /* Batch */ = { - isa = PBXGroup; - children = ( - 0FFFFFFF000000000000C141 /* InstalledAppEnumerator.swift */, - 0FFFFFFF000000000000C142 /* BatchAppResult.swift */, - 0FFFFFFF000000000000C143 /* BatchAnalyzer.swift */, - ); - path = Batch; - sourceTree = ""; - }; 0CCCCCCC0000000000000C00 /* privacycommandCore */ = { isa = PBXGroup; children = ( @@ -748,6 +750,16 @@ path = Updates; sourceTree = ""; }; + 0CCCCCCC0000000000000C14 /* Batch */ = { + isa = PBXGroup; + children = ( + 0FFFFFFF000000000000C141 /* InstalledAppEnumerator.swift */, + 0FFFFFFF000000000000C142 /* BatchAppResult.swift */, + 0FFFFFFF000000000000C143 /* BatchAnalyzer.swift */, + ); + path = Batch; + sourceTree = ""; + }; 0CCCCCCC0000000000000C20 /* privacycommandGuestProtocol */ = { isa = PBXGroup; children = ( @@ -805,6 +817,7 @@ isa = PBXNativeTarget; buildConfigurationList = 4400000000000000000000A0 /* Build configuration list for PBXNativeTarget "privacycommand" */; buildPhases = ( + 6817230F7A024CABA7EF1475 /* Prepare version and build identity */, 0202020200000000000000A0 /* Sources */, 0202020200000000000000A1 /* Frameworks */, 0202020200000000000000A2 /* Resources */, @@ -843,6 +856,7 @@ isa = PBXNativeTarget; buildConfigurationList = A954CFF02F9E0506005AC054 /* Build configuration list for PBXNativeTarget "privacycommandHelper" */; buildPhases = ( + E456358CD2407044374546AD /* Prepare version and build identity */, A954CFE62F9E0506005AC054 /* Sources */, A954CFE72F9E0506005AC054 /* Frameworks */, A954CFE82F9E0506005AC054 /* CopyFiles */, @@ -855,8 +869,6 @@ A954CFEB2F9E0506005AC054 /* privacycommandHelper */, ); name = privacycommandHelper; - packageProductDependencies = ( - ); productName = privacycommandHelper; productReference = A954CFEA2F9E0506005AC054 /* privacycommandHelper */; productType = "com.apple.product-type.tool"; @@ -942,7 +954,52 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/bash; - shellScript = "# Build the privacycommand-guest executable via SwiftPM, copy it\n# into the app's Resources folder, and codesign it with hardened\n# runtime. Apple's notarization service rejects any embedded Mach-O\n# that wasn't built with --options runtime, and SwiftPM (unlike\n# xcodebuild) doesn't apply that flag automatically — so we re-sign\n# here, using the same identity Xcode is using for the app target.\n\nset -euo pipefail\n\nTARGET=privacycommand-guest\necho \"Building ${TARGET} via SwiftPM…\"\ncd \"${SRCROOT}\"\nswift build -c release --product \"${TARGET}\"\n\nGUEST_BIN=\"${SRCROOT}/.build/release/${TARGET}\"\nif [[ ! -f \"${GUEST_BIN}\" ]]; then\n echo \"error: SwiftPM didn't produce ${GUEST_BIN}\" >&2\n exit 1\nfi\n\nDEST=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\"\nmkdir -p \"${DEST}\"\ncp -f \"${GUEST_BIN}\" \"${DEST}/${TARGET}\"\nchmod +x \"${DEST}/${TARGET}\"\n\n# Re-sign with hardened runtime. EXPANDED_CODE_SIGN_IDENTITY is the\n# resolved cert ID Xcode will use for the rest of the app. For\n# Sign-to-Run-Locally builds it's '-' (ad-hoc); for Developer ID\n# Release builds it's the actual cert. We always pass --options\n# runtime so notarization accepts the binary; we add --timestamp\n# only when signing with a real identity (ad-hoc + --timestamp\n# fails because Apple's TSA needs the cert to validate against).\nIDENTITY=\"${EXPANDED_CODE_SIGN_IDENTITY:-${CODE_SIGN_IDENTITY:--}}\"\nif [[ -z \"${IDENTITY}\" ]]; then\n IDENTITY=\"-\"\nfi\nSIGN_FLAGS=( --force --options runtime --sign \"${IDENTITY}\" )\nif [[ \"${IDENTITY}\" != \"-\" ]]; then\n SIGN_FLAGS+=( --timestamp )\nfi\necho \"Signing ${TARGET} with identity '${IDENTITY}'…\"\n/usr/bin/codesign \"${SIGN_FLAGS[@]}\" \"${DEST}/${TARGET}\"\n\necho \"Staged + signed ${TARGET} into ${DEST}/\"\n"; + shellScript = "# Build the privacycommand-guest executable via SwiftPM, copy it\n# into the app's Resources folder, and codesign it with hardened\n# runtime. Apple's notarization service rejects any embedded Mach-O\n# that wasn't built with --options runtime, and SwiftPM (unlike\n# xcodebuild) doesn't apply that flag automatically — so we re-sign\n# here, using the same identity Xcode is using for the app target.\n\nset -euo pipefail\n\nTARGET=privacycommand-guest\necho \"Building ${TARGET} via SwiftPM…\"\ncd \"${SRCROOT}\"\nswift build -c release --product \"${TARGET}\"\n\nGUEST_BIN=\"${SRCROOT}/.build/release/${TARGET}\"\nif [[ ! -f \"${GUEST_BIN}\" ]]; then\n echo \"error: SwiftPM didn't produce ${GUEST_BIN}\" >&2\n exit 1\nfi\n\nDEST=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\"\nmkdir -p \"${DEST}\"\ncp -f \"${GUEST_BIN}\" \"${DEST}/${TARGET}\"\nchmod +x \"${DEST}/${TARGET}\"\n\n# Follow Xcode's signing choice for unsigned archive validation.\nif [[ \"${CODE_SIGNING_ALLOWED:-YES}\" == \"NO\" ]]; then\n echo \"Staged ${TARGET} without signing (CODE_SIGNING_ALLOWED=NO).\"\n exit 0\nfi\n\n# Re-sign with hardened runtime. EXPANDED_CODE_SIGN_IDENTITY is the\n# resolved cert ID Xcode will use for the rest of the app. For\n# Sign-to-Run-Locally builds it's '-' (ad-hoc); for Developer ID\n# Release builds it's the actual cert. We always pass --options\n# runtime so notarization accepts the binary; we add --timestamp\n# only when signing with a real identity (ad-hoc + --timestamp\n# fails because Apple's TSA needs the cert to validate against).\nIDENTITY=\"${EXPANDED_CODE_SIGN_IDENTITY:-${CODE_SIGN_IDENTITY:--}}\"\nif [[ -z \"${IDENTITY}\" ]]; then\n IDENTITY=\"-\"\nfi\nSIGN_FLAGS=( --force --options runtime --sign \"${IDENTITY}\" )\nif [[ \"${IDENTITY}\" != \"-\" ]]; then\n SIGN_FLAGS+=( --timestamp )\nfi\necho \"Signing ${TARGET} with identity '${IDENTITY}'…\"\n/usr/bin/codesign \"${SIGN_FLAGS[@]}\" \"${DEST}/${TARGET}\"\n\necho \"Staged + signed ${TARGET} into ${DEST}/\"\n"; + }; + 6817230F7A024CABA7EF1475 /* Prepare version and build identity */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/../scripts/prepare-build-info.py", + "$(PROJECT_TEMP_DIR)/BuildIdentity.plist", + "$(SRCROOT)/Resources/Info.plist", + ); + name = "Prepare version and build identity"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/BuildIdentity-Info.plist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "env -u SDKROOT /usr/bin/python3 \"$SRCROOT/../scripts/prepare-build-info.py\" plist\n"; + }; + E456358CD2407044374546AD /* Prepare version and build identity */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/../scripts/prepare-build-info.py", + "$(PROJECT_TEMP_DIR)/BuildIdentity.plist", + ); + name = "Prepare version and build identity"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/BuildIdentity-Info.plist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "env -u SDKROOT /usr/bin/python3 \"$SRCROOT/../scripts/prepare-build-info.py\" plist\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -1145,26 +1202,25 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUILD_IDENTITY_SOURCE_PLIST = "$(SRCROOT)/Resources/Info.plist"; CODE_SIGN_ENTITLEMENTS = Resources/privacycommand.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 6S9Q286XS9; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = Resources/Info.plist; + INFOPLIST_FILE = "$(DERIVED_FILE_DIR)/BuildIdentity-Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 0.1.5; - PRODUCT_BUNDLE_IDENTIFIER = org.privacykey.privacycommand; OTHER_LDFLAGS = "-lsqlite3"; + PRODUCT_BUNDLE_IDENTIFIER = org.privacykey.privacycommand; PRODUCT_NAME = privacycommand; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -1175,26 +1231,25 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUILD_IDENTITY_SOURCE_PLIST = "$(SRCROOT)/Resources/Info.plist"; CODE_SIGN_ENTITLEMENTS = Resources/privacycommand.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 6S9Q286XS9; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = Resources/Info.plist; + INFOPLIST_FILE = "$(DERIVED_FILE_DIR)/BuildIdentity-Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 0.1.5; - PRODUCT_BUNDLE_IDENTIFIER = org.privacykey.privacycommand; OTHER_LDFLAGS = "-lsqlite3"; + PRODUCT_BUNDLE_IDENTIFIER = org.privacykey.privacycommand; PRODUCT_NAME = privacycommand; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -1241,6 +1296,7 @@ }; 3300000000000000000000F1 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 45C386EC5EE885DE4E4DFA6F /* Shared.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -1303,6 +1359,7 @@ }; 3300000000000000000000F2 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 45C386EC5EE885DE4E4DFA6F /* Shared.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -1366,7 +1423,8 @@ DEVELOPMENT_TEAM = 6S9Q286XS9; ENABLE_HARDENED_RUNTIME = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; - GENERATE_INFOPLIST_FILE = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "$(DERIVED_FILE_DIR)/BuildIdentity-Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "privacycommand Helper"; INFOPLIST_KEY_LSBackgroundOnly = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "privacycommand Helper — privileged helper for privacycommand. A privacykey product."; @@ -1393,7 +1451,8 @@ DEVELOPMENT_TEAM = 6S9Q286XS9; ENABLE_HARDENED_RUNTIME = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; - GENERATE_INFOPLIST_FILE = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "$(DERIVED_FILE_DIR)/BuildIdentity-Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "privacycommand Helper"; INFOPLIST_KEY_LSBackgroundOnly = YES; INFOPLIST_KEY_NSHumanReadableCopyright = "privacycommand Helper — privileged helper for privacycommand. A privacykey product."; diff --git a/privacycommand/privacycommand.xcodeproj/xcshareddata/xcschemes/privacycommand.xcscheme b/privacycommand/privacycommand.xcodeproj/xcshareddata/xcschemes/privacycommand.xcscheme index 14b1c46..afeb77d 100644 --- a/privacycommand/privacycommand.xcodeproj/xcshareddata/xcschemes/privacycommand.xcscheme +++ b/privacycommand/privacycommand.xcodeproj/xcshareddata/xcschemes/privacycommand.xcscheme @@ -5,6 +5,15 @@ + + + + + + + + + /dev/null | head -1 || true) +fi +version="${version:-0.0.0}" + +if [ -t 1 ]; then bold=$'\033[1m'; dim=$'\033[2m'; amber=$'\033[33m'; off=$'\033[0m' +else bold=""; dim=""; amber=""; off=""; fi + +printf '%s▸ %s %s (%s)%s\n' "$bold" "$name" "$version" "$BUILD_NUMBER" "$off" +if [ -n "$BUILD_SHA" ]; then + printf ' commit %s branch %s\n' "$BUILD_SHA" "$BUILD_BRANCH" +else + printf ' commit %s(none recorded — no repository)%s\n' "$dim" "$off" +fi +if [ "$BUILD_DIRTY" = YES ]; then + printf ' tree %s⚠ DIRTY — %s files (diff %s)%s\n' "$amber" "$BUILD_DIRTY_FILES" "$BUILD_DIFF_ID" "$off" + printf ' %sthis build matches no commit and cannot be reproduced%s\n' "$amber" "$off" +elif [ "$BUILD_TAGGED" = YES ]; then + printf ' tree clean · %s\n' "$BUILD_DESCRIBE" +else + printf ' tree clean · %s%s%s\n' "$dim" "${BUILD_DESCRIBE:-untagged}" "$off" +fi diff --git a/scripts/build_identity.rb b/scripts/build_identity.rb new file mode 100644 index 0000000..12f2b21 --- /dev/null +++ b/scripts/build_identity.rb @@ -0,0 +1,31 @@ +# Shared Fastlane adapter. Capture once, then reuse these settings for every +# artifact in a lane. Empty BUILD_NUMBER input means generate, never build 0. +require "open3" +require "shellwords" +require "time" + +module BuildIdentity + def self.settings(channel) + root = File.expand_path("..", __dir__) + output, result = Open3.capture2( + { "BUILD_CHANNEL" => channel }, "bash", File.join(__dir__, "buildinfo.sh"), chdir: root + ) + raise "Could not generate build identity" unless result.success? + values = output.lines.map(&:strip).reject(&:empty?) + override = ENV["BUILD_NUMBER"].to_s + unless override.empty? + raise "Invalid BUILD_NUMBER: #{override}" unless override.match?(/\A[0-9]{4}\.[0-9]{4}\.[0-9]{4}\z/) + timestamp = ENV["BUILD_TIMESTAMP"].to_s + timestamp = Time.strptime(override + " +0000", "%Y.%m%d.%H%M %z").utc.iso8601 if timestamp.empty? + raise "BUILD_TIMESTAMP differs from BUILD_NUMBER" unless Time.iso8601(timestamp).utc.strftime("%Y.%m%d.%H%M") == override + values.reject! { |value| value.start_with?("BUILD_NUMBER=", "BUILD_TIMESTAMP=") } + values << "BUILD_NUMBER=#{override}" + values << "BUILD_TIMESTAMP=#{timestamp}" + end + values + end + + def self.xcargs(channel) + settings(channel).map(&:shellescape).join(" ") + end +end diff --git a/scripts/buildinfo.sh b/scripts/buildinfo.sh new file mode 100755 index 0000000..58a852c --- /dev/null +++ b/scripts/buildinfo.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Emits xcodebuild build-setting arguments describing the tree being built. +# +# xcodebuild ... $(Scripts/buildinfo.sh) +# +# BUILD_CHANNEL local (default) | ci | testflight | release +# BUILD_PROVENANCE full (default) | minimal — `minimal` omits SHA and branch, +# set automatically for the `release` channel so a public +# binary carries nothing about a private repo. +set -euo pipefail + +channel="${BUILD_CHANNEL:-local}" +provenance="${BUILD_PROVENANCE:-$([ "$channel" = release ] && echo minimal || echo full)}" + +# Absent or unreadable repository (source export, artefact-only CI job) must +# degrade to empty provenance, never abort the build. `set -e` plus `pipefail` +# will kill an assignment whose pipeline fails, so every git call is guarded. +sha=""; branch=""; describe=""; diffid=""; tagged=NO; n=0 + +if git rev-parse --git-dir >/dev/null 2>&1 && git rev-parse HEAD >/dev/null 2>&1; then + sha=$(git rev-parse --short=8 HEAD) + branch=$(git rev-parse --abbrev-ref HEAD) + n=$(git status --porcelain | wc -l | tr -d ' ') + describe=$(git describe --tags --always --dirty 2>/dev/null || echo "") + if git describe --tags --exact-match --match 'v[0-9]*' HEAD >/dev/null 2>&1; then tagged=YES; fi + # Hash tracked changes AND untracked content — `git diff HEAD` alone ignores + # untracked files, so a tree dirty only with new files would otherwise get + # the constant SHA-256 of empty input (e3b0c442) on every build. + if [ "$n" -gt 0 ]; then + diffid=$( { git diff HEAD; git status --porcelain; + git ls-files --others --exclude-standard -z \ + | xargs -0 shasum -a 256 2>/dev/null || true; + } | shasum -a 256 | cut -c1-8 ) + fi +fi + +# `release` builds ship no repository detail. Keys stay present but empty so +# the plist shape is identical across channels; BuildInfo.swift treats an empty +# value as absent. +if [ "$provenance" = minimal ]; then sha=""; branch=""; describe=""; diffid=""; fi + +# Capture the clock once: the number and timestamp must describe the same +# instant even when this invocation straddles a UTC minute/day boundary. +stamp=$(date -u '+%Y.%m%d.%H%M|%Y-%m-%dT%H:%M:%SZ') +cat <&2 - echo " Make sure Xcode has Marketing Version set under Project →" >&2 - echo " privacycommand → General → Identity." >&2 + echo "error: could not read MARKETING_VERSION from Config/Shared.xcconfig" >&2 exit 2 fi echo "Building privacycommand v$VERSION" -# Auto-incrementing CFBundleVersion: count of git commits on HEAD. -# Sparkle requires CFBundleVersion to monotonically increase across -# releases, and `git rev-list --count HEAD` grows with every push to -# main, so we get that for free. CI does fetch-depth: 0 so this works -# in GitHub Actions; locally the result is whatever your full clone -# has, which is also fine for dry-runs. -BUILD_NUMBER="$(git -C "$REPO_ROOT" rev-list --count HEAD)" +# Capture one UTC build number and provenance stamp for the release. +build_identity=() +while IFS= read -r setting; do build_identity+=("$setting"); done < <(cd "$REPO_ROOT" && BUILD_CHANNEL=release scripts/buildinfo.sh) +BUILD_NUMBER="${build_identity[0]#BUILD_NUMBER=}" echo "Using CFBundleVersion: $BUILD_NUMBER" # ── 2. Resolve the signing identity ──────────────────────────────── @@ -147,7 +132,7 @@ xcodebuild \ CODE_SIGN_IDENTITY="$DEVELOPER_ID" \ CODE_SIGN_STYLE=Manual \ DEVELOPMENT_TEAM="$TEAM_ID" \ - CURRENT_PROJECT_VERSION="$BUILD_NUMBER" \ + "${build_identity[@]}" \ archive # ── 4. Export the .app from the archive ──────────────────────────── diff --git a/scripts/test-archive-command.py b/scripts/test-archive-command.py new file mode 100644 index 0000000..c991680 --- /dev/null +++ b/scripts/test-archive-command.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Check archive signing defaults and Organizer/extension verification.""" +import json +import os +from pathlib import Path +import plistlib +import shutil +import subprocess +import tempfile + +kit = Path(__file__).resolve().parents[1] +with tempfile.TemporaryDirectory() as directory: + root = Path(directory).resolve() + shutil.copytree(kit / "scripts", root / "Scripts") + (root / "Config").mkdir() + (root / "Config/ArchiveTargets.json").write_text(json.dumps({ + "project": "App/Fixture.xcodeproj", "spec": "App/project.yml", "defaultPlatform": "macos", + "platforms": {"macos": {"scheme": "Fixture", "destination": "generic/platform=macOS"}}, + })) + runner = root / "Scripts/archive-app.py" + env = dict(os.environ, APPLE_TEAM_ID="ABCDEFGHIJ") + commands = json.loads(subprocess.check_output(["python3", str(runner), "--plan"], env=env)) + assert commands[0] == ["xcodegen", "generate", "--spec", "App/project.yml"] + assert "-allowProvisioningUpdates" in commands[1] and "DEVELOPMENT_TEAM=ABCDEFGHIJ" in commands[1] + assert str(root / "build/Fixture-macos.xcarchive") in commands[1] + assert not any(argument.startswith(("BUILD_NUMBER=", "CURRENT_PROJECT_VERSION=")) for argument in commands[1]) + unsigned = json.loads(subprocess.check_output(["python3", str(runner), "--unsigned", "--plan"], env=env)) + assert "CODE_SIGNING_ALLOWED=NO" in unsigned[1] and "-allowProvisioningUpdates" not in unsigned[1] + failure = subprocess.run(["python3", str(runner), "--plan"], env=dict(env, APPLE_TEAM_ID="invalid"), capture_output=True) + assert failure.returncode != 0 + archive = root / "Fixture.xcarchive" + app = archive / "Products/Applications/Fixture.app" + app.mkdir(parents=True) + identity = {"CFBundleVersion": "2026.0930.1234", "CFBundleShortVersionString": "0.1.0", + "BuildTimestamp": "2026-09-30T12:34:56Z", "BuildChannel": "release"} + (archive / "Info.plist").write_bytes(plistlib.dumps({"ApplicationProperties": { + "ApplicationPath": "Applications/Fixture.app", "CFBundleVersion": identity["CFBundleVersion"], + "CFBundleShortVersionString": identity["CFBundleShortVersionString"], + }})) + (app / "Info.plist").write_bytes(plistlib.dumps(identity)) + extension = app / "PlugIns/FixtureWidget.appex" + extension.mkdir(parents=True) + (extension / "Info.plist").write_bytes(plistlib.dumps(identity)) + resources = extension / "LibraryResources.bundle" + resources.mkdir() + (resources / "Info.plist").write_bytes(plistlib.dumps({"CFBundleIdentifier": "library.resources"})) + verify = ["python3", str(root / "Scripts/verify-archive.py"), str(archive)] + subprocess.run(verify, check=True) + (extension / "Info.plist").write_bytes(plistlib.dumps(dict(identity, CFBundleVersion="1"))) + failure = subprocess.run(verify, capture_output=True) + assert failure.returncode != 0 +print("Archive checks passed: signing defaults, automatic capture, Organizer/extension parity") diff --git a/scripts/test-build-identity.py b/scripts/test-build-identity.py new file mode 100644 index 0000000..b6fe6c9 --- /dev/null +++ b/scripts/test-build-identity.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Exercise scheme capture and target plist preparation with real reference scripts.""" +import os +from pathlib import Path +import plistlib +import re +import shutil +import subprocess +import tempfile + +kit = Path(__file__).resolve().parents[1] +with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + shutil.copytree(kit / "scripts", root / "Scripts") + script = root / "Scripts/prepare-build-info.py" + env = dict(os.environ, PROJECT_TEMP_DIR=str(root / "project"), DERIVED_FILE_DIR=str(root / "target"), + MARKETING_VERSION="0.1.0", CURRENT_PROJECT_VERSION="0", BUILD_NUMBER="0", + BUILD_TIMESTAMP="", BUILD_CHANNEL="xcode", PRODUCT_NAME="Fixture", + PRODUCT_BUNDLE_IDENTIFIER="example.fixture", EXECUTABLE_NAME="Fixture", + PRODUCT_BUNDLE_PACKAGE_TYPE="APPL", PLATFORM_NAME="iphoneos", ACTION="install", + INFOPLIST_KEY_UILaunchScreen_Generation="YES") + env.pop("BUILD_PROVENANCE", None) + subprocess.run(["python3", str(script), "session"], env=env, check=True) + values = plistlib.loads((root / "project/BuildIdentity.plist").read_bytes()) + assert re.fullmatch(r"[0-9]{4}\.[0-9]{4}\.[0-9]{4}", values["BUILD_NUMBER"]) + assert values["BUILD_CHANNEL"] == "release" + for name, source in [("App", {"NSCameraUsageDescription": "Keep this permission", "CFBundleDocumentTypes": [{"CFBundleTypeName": "Fixture"}]}), + ("Extension", {"NSExtension": {"NSExtensionPointIdentifier": "com.apple.share-services"}})]: + path = root / (name + ".plist") + path.write_bytes(plistlib.dumps(source)) + target_env = dict(env, BUILD_IDENTITY_SOURCE_PLIST=str(path), DERIVED_FILE_DIR=str(root / name)) + subprocess.run(["python3", str(script), "plist"], env=target_env, check=True) + actual = plistlib.loads((root / name / "BuildIdentity-Info.plist").read_bytes()) + assert all(actual[key] == value for key, value in source.items()) + assert actual["CFBundleVersion"] == values["BUILD_NUMBER"] + assert actual["CFBundleShortVersionString"] == "0.1.0" + assert actual["BuildTimestamp"] == values["BUILD_TIMESTAMP"] + assert actual["BuildChannel"] == "release" and actual["BuildSHA"] == "" + assert actual["CFBundleExecutable"] == "Fixture" and "UILaunchScreen" in actual + # A complete wrapper stamp wins even when a second platform captures later. + env.update(BUILD_NUMBER="2026.0930.0000", CURRENT_PROJECT_VERSION="2026.0930.0000", + BUILD_TIMESTAMP="2026-09-30T00:00:00Z", BUILD_CHANNEL="testflight", + BUILD_SHA="12345678", BUILD_BRANCH="feature/fixture", BUILD_DIRTY="NO", + BUILD_DIRTY_FILES="0", BUILD_DIFF_ID="", BUILD_DESCRIBE="fixture", BUILD_TAGGED="NO") + subprocess.run(["python3", str(script), "plist"], env=env, check=True) + actual = plistlib.loads((root / "target/BuildIdentity-Info.plist").read_bytes()) + assert actual["CFBundleVersion"] == "2026.0930.0000" and actual["BuildSHA"] == "12345678" + assert actual["BuildTimestamp"] == "2026-09-30T00:00:00Z" + # Invalid sentinels/marketing values fail before Xcode processes the plist. + env["MARKETING_VERSION"] = "1.0-beta" + failure = subprocess.run(["python3", str(script), "plist"], env=env, capture_output=True) + assert failure.returncode != 0 + # If capture fails, the previous session cannot silently stamp a new archive. + (root / "Scripts/buildinfo.sh").write_text("exit 1\n") + failure = subprocess.run(["python3", str(script), "session"], env=env, capture_output=True) + assert failure.returncode != 0 and not (root / "project/BuildIdentity.plist").exists() + failure = subprocess.run(["python3", str(script), "plist"], env=env, capture_output=True) + assert failure.returncode != 0 and b"Build identity is missing" in failure.stderr +print("Xcode identity passed: Archive capture, bundled target parity, metadata preservation, overrides") diff --git a/scripts/test-fastlane-identity.rb b/scripts/test-fastlane-identity.rb new file mode 100644 index 0000000..6f7a8a9 --- /dev/null +++ b/scripts/test-fastlane-identity.rb @@ -0,0 +1,31 @@ +require "tmpdir" +require "fileutils" + +kit = File.expand_path("..", __dir__) +Dir.mktmpdir do |root| + FileUtils.cp_r(File.join(kit, "scripts"), File.join(root, "Scripts")) + require File.join(root, "Scripts/build_identity") + saved = ENV.to_h + begin + [nil, ""].each do |input| + input.nil? ? ENV.delete("BUILD_NUMBER") : ENV["BUILD_NUMBER"] = input + values = BuildIdentity.settings("release").to_h { |value| value.split("=", 2) } + raise "Missing/empty input must generate a stamp" unless values["BUILD_NUMBER"].match?(/\A[0-9]{4}\.[0-9]{4}\.[0-9]{4}\z/) + raise "Release provenance must be minimal" unless values["BUILD_SHA"] == "" && values["BUILD_BRANCH"] == "" + end + ENV["BUILD_NUMBER"] = "2026.0930.0000" + ENV.delete("BUILD_TIMESTAMP") + raise "Explicit override lost" unless BuildIdentity.settings("release").include?("BUILD_NUMBER=2026.0930.0000") + raise "Override timestamp differs" unless BuildIdentity.settings("release").include?("BUILD_TIMESTAMP=2026-09-30T00:00:00Z") + ENV["BUILD_NUMBER"] = "0" + begin + BuildIdentity.settings("release") + raise "Zero must never ship" + rescue RuntimeError => error + raise unless error.message.start_with?("Invalid BUILD_NUMBER") + end + ensure + ENV.replace(saved) + end +end +puts "Fastlane adapter passed: missing/empty input, release redaction, override, invalid sentinel" diff --git a/scripts/verify-archive.py b/scripts/verify-archive.py new file mode 100644 index 0000000..e60f170 --- /dev/null +++ b/scripts/verify-archive.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Verify an Xcode archive's version/build metadata and bundled extensions.""" +import argparse +from datetime import datetime +from pathlib import Path +import plistlib +import re + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("archive", type=Path) +args = parser.parse_args() +archive = args.archive +metadata = plistlib.loads((archive / "Info.plist").read_bytes())["ApplicationProperties"] +app = archive / "Products" / metadata["ApplicationPath"] +main_plist = app / "Contents/Info.plist" if (app / "Contents/Info.plist").is_file() else app / "Info.plist" +main = plistlib.loads(main_plist.read_bytes()) +build = main["CFBundleVersion"] +version = main["CFBundleShortVersionString"] +assert re.fullmatch(r"[0-9]{4}\.[0-9]{4}\.[0-9]{4}", build), f"Unexpected build format: {build}" +assert metadata["CFBundleVersion"] == build, "Organizer build differs from the app" +assert metadata["CFBundleShortVersionString"] == version, "Organizer version differs from the app" +assert datetime.strptime(main["BuildTimestamp"], "%Y-%m-%dT%H:%M:%SZ").strftime("%Y.%m%d.%H%M") == build +assert main["BuildChannel"] in ("release", "testflight") +if main["BuildChannel"] == "release": + assert all(main.get(key, "") == "" for key in ("BuildSHA", "BuildBranch", "BuildDescribe", "BuildDiffID")) +plists = [main_plist] +for bundle in app.rglob("*.appex"): + if "Frameworks" in bundle.relative_to(app).parts: + continue + path = bundle / "Contents/Info.plist" if (bundle / "Contents/Info.plist").is_file() else bundle / "Info.plist" + plists.append(path) +for path in plists: + info = plistlib.loads(path.read_bytes()) + assert info["CFBundleVersion"] == build, path + assert info["CFBundleShortVersionString"] == version, path + assert info["BuildTimestamp"] == main["BuildTimestamp"], path + print(f"{path.relative_to(archive)}: {version} ({build})") +print("Archive verified: Organizer, app and extension identity; UTC timestamp and release provenance")