diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..4ad2ac6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [zywkloo] diff --git a/.github/workflows/release-temp-dmg.yml b/.github/workflows/release-temp-dmg.yml new file mode 100644 index 0000000..3d3b998 --- /dev/null +++ b/.github/workflows/release-temp-dmg.yml @@ -0,0 +1,63 @@ +name: Release Temp DMG + +on: + push: + branches: + - develop + workflow_dispatch: + +jobs: + temp-dmg: + runs-on: macos-15 + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Select Xcode 16 + run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + + - name: Install XcodeGen + run: brew install xcodegen + + - name: Generate Xcode project + run: xcodegen generate + + - name: Build Release app (unsigned) + run: | + xcodebuild build \ + -project EDFViewerMac.xcodeproj \ + -scheme EDFViewerMac \ + -configuration Release \ + -destination 'platform=macOS' \ + -derivedDataPath ./.derivedData-temp \ + CODE_SIGN_IDENTITY=- \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO + + - name: Package temporary DMG + run: | + APP_PATH="$(find ./.derivedData-temp/Build/Products/Release -maxdepth 1 -name '*.app' -type d | head -1)" + echo "App: $APP_PATH" + + # Ad-hoc sign for local testing scenarios. This is not notarized. + codesign --force --deep --sign - "$APP_PATH" + + mkdir -p dist + DMG_PATH="dist/EDFViewer-temp-${GITHUB_RUN_NUMBER}.dmg" + hdiutil create \ + -volname "EDFViewer Temp" \ + -srcfolder "$APP_PATH" \ + -ov \ + -format UDZO \ + "$DMG_PATH" + + echo "DMG_PATH=$DMG_PATH" >> "$GITHUB_ENV" + + - name: Upload DMG artifact + uses: actions/upload-artifact@v4 + with: + name: temp-dmg + path: ${{ env.DMG_PATH }} + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index afb850f..c89adaa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,74 @@ jobs: - name: Generate Xcode project run: xcodegen generate - - name: Archive + - name: Ensure tag commit is on main + id: main_check + continue-on-error: true + run: | + git fetch origin main --depth=1 + if git merge-base --is-ancestor "$GITHUB_SHA" "origin/main"; then + echo "Tag commit is on main." + echo "on_main=true" >> "$GITHUB_OUTPUT" + else + echo "Tag commit is not on main. Release will be skipped." + echo "on_main=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + - name: Determine signing mode + id: signing + env: + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }} + MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} + run: | + if [[ "${{ steps.main_check.outputs.on_main }}" != "true" ]]; then + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "reason=not_on_main" >> "$GITHUB_OUTPUT" + echo "Skipping: tag commit is not on main." + elif [[ -n "${APPLE_TEAM_ID}" && -n "${APPLE_ID}" && -n "${APPLE_APP_SPECIFIC_PASSWORD}" && -n "${MACOS_CERT_P12_BASE64}" && -n "${MACOS_CERT_PASSWORD}" ]]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + echo "reason=ready" >> "$GITHUB_OUTPUT" + echo "Release mode: signed + notarized" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "reason=missing_secrets" >> "$GITHUB_OUTPUT" + echo "Skipping: missing signing/notarization secrets." + fi + + - name: Import Developer ID certificate + if: steps.signing.outputs.enabled == 'true' + env: + MACOS_CERT_P12_BASE64: ${{ secrets.MACOS_CERT_P12_BASE64 }} + MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} + run: | + CERT_PATH="$RUNNER_TEMP/dev_id_cert.p12" + KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db" + KEYCHAIN_PASSWORD="${KEYCHAIN_PASSWORD:-temp-keychain-password}" + + echo "$MACOS_CERT_P12_BASE64" | base64 --decode > "$CERT_PATH" + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security list-keychains -d user -s "$KEYCHAIN_PATH" + + security import "$CERT_PATH" \ + -k "$KEYCHAIN_PATH" \ + -P "$MACOS_CERT_PASSWORD" \ + -T /usr/bin/codesign \ + -T /usr/bin/security \ + -T /usr/bin/xcodebuild + + security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + - name: Archive (signed) + if: steps.signing.outputs.enabled == 'true' + env: + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | xcodebuild archive \ -project EDFViewerMac.xcodeproj \ @@ -31,13 +98,40 @@ jobs: -configuration Release \ -destination 'platform=macOS' \ -archivePath build/EDFViewerMac.xcarchive \ - CODE_SIGN_IDENTITY=- \ - CODE_SIGNING_REQUIRED=NO \ - CODE_SIGNING_ALLOWED=NO + APPLE_TEAM_ID="$APPLE_TEAM_ID" + + - name: Export signed app + if: steps.signing.outputs.enabled == 'true' + env: + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + cat > build/ExportOptions.plist < + + + + method + developer-id + signingStyle + manual + teamID + ${APPLE_TEAM_ID} + + + PLIST + + xcodebuild -exportArchive \ + -archivePath build/EDFViewerMac.xcarchive \ + -exportPath build/export \ + -exportOptionsPlist build/ExportOptions.plist - name: Package DMG + env: + SIGNING_ENABLED: ${{ steps.signing.outputs.enabled }} run: | - APP=$(find build/EDFViewerMac.xcarchive -name "*.app" -type d | head -1) + if [[ "$SIGNING_ENABLED" == "true" ]]; then + APP=$(find build/export -name "*.app" -type d | head -1) + fi echo "Packaging: $APP" mkdir -p dist hdiutil create \ @@ -47,7 +141,28 @@ jobs: -format UDZO \ "dist/EDFViewerMac-${{ github.ref_name }}.dmg" + - name: Sign and notarize DMG + if: steps.signing.outputs.enabled == 'true' + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + DMG="dist/EDFViewerMac-${{ github.ref_name }}.dmg" + + codesign --force --sign "Developer ID Application" --timestamp "$DMG" + + xcrun notarytool submit "$DMG" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + + xcrun stapler staple "$DMG" + spctl -a -t open --context context:primary-signature -v "$DMG" + - name: Create GitHub Release + if: steps.signing.outputs.enabled == 'true' env: GH_TOKEN: ${{ github.token }} run: | @@ -60,8 +175,14 @@ jobs: ### Install 1. Download \`EDFViewerMac-${{ github.ref_name }}.dmg\` 2. Open the DMG and drag the app to Applications - 3. Right-click → Open on first launch (app is not yet notarized) + 3. Open normally from Applications ### Requirements macOS 13 or later" \ "dist/EDFViewerMac-${{ github.ref_name }}.dmg" + + - name: Skip notice + if: steps.signing.outputs.enabled != 'true' + run: | + echo "Release skipped." + echo "Reason: ${{ steps.signing.outputs.reason }}" diff --git a/.gitignore b/.gitignore index 15e03ad..8eeed11 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ xcuserdata/ ## Build build/ DerivedData/ +/.derivedData/ +/.derivedData2/ +/.derivedData*/ .build/ ## User settings diff --git a/EDFViewerMac.xcodeproj/project.pbxproj b/EDFViewerMac.xcodeproj/project.pbxproj deleted file mode 100644 index 7d564e3..0000000 --- a/EDFViewerMac.xcodeproj/project.pbxproj +++ /dev/null @@ -1,498 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 77; - objects = { - -/* Begin PBXBuildFile section */ - 0189F499CBA2FA02C568C8DF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6F4AA165B20C7A22C4666652 /* Assets.xcassets */; }; - 0420D985CD73DAAADB1381FF /* RealEDFReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 685039C6BD8E44625F389F7F /* RealEDFReader.swift */; }; - 331E6A3E3DD6153B97ACEE5E /* EDFViewerMacApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBDB47CF168549400858F67 /* EDFViewerMacApp.swift */; }; - 38EBB60755D0D4BF96168425 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277E64FC6A22251D3963489E /* Models.swift */; }; - 4893F89CFEA376361F6BCE3F /* ViewerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74E5E248D171C4D22769FF1 /* ViewerViewModel.swift */; }; - 4F0FA489159F9E0F24EBBA9B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BBBEE954FC91DD87D35C0F6 /* ContentView.swift */; }; - 522181A6606FED6AD7AACD89 /* Fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 72A5CC800534BE9F6ABF0271 /* Fixtures */; }; - 52C55A2071571C081BF044C8 /* EDFReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF8C31DFA559F5C09DCB5F76 /* EDFReader.swift */; }; - 5E30C7E6FC222DD0CCF2C6CA /* SignalProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D77077467CB2C00AC9729E /* SignalProcessing.swift */; }; - 79EC8ACF0C645A877E4088ED /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277E64FC6A22251D3963489E /* Models.swift */; }; - 88357D1328CDEFDFA10F3BC6 /* ViewerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0D30423A2EA593113195FE7 /* ViewerViewModelTests.swift */; }; - 8C13F9FE1B2B30AE67962BD0 /* ViewerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74E5E248D171C4D22769FF1 /* ViewerViewModel.swift */; }; - 8C24A0BC681D828230745D92 /* WaveformMinMaxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38E06C1F78853D5E4E61C0D /* WaveformMinMaxView.swift */; }; - 9AE0AC69672428C5C9066DA1 /* SignalProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D77077467CB2C00AC9729E /* SignalProcessing.swift */; }; - A12C86EB7DB490B17FCA5669 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BBBEE954FC91DD87D35C0F6 /* ContentView.swift */; }; - B6506E56AAE7144B26E32235 /* EDFReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3691C97574D02C73304ACA8 /* EDFReaderTests.swift */; }; - B7CB8881E1070FBF4BD44ED1 /* WaveformMinMaxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38E06C1F78853D5E4E61C0D /* WaveformMinMaxView.swift */; }; - BC0E02DFE3122F4782538402 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23549711775AE252DC4F2904 /* SettingsView.swift */; }; - C4626A72D53262A2C2DD3E5A /* EDFReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF8C31DFA559F5C09DCB5F76 /* EDFReader.swift */; }; - C5D7FF78E15B1BF29E738205 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6F4AA165B20C7A22C4666652 /* Assets.xcassets */; }; - CB97548711B45BF303A56023 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23549711775AE252DC4F2904 /* SettingsView.swift */; }; - E47C6F9427DB7F204972E201 /* RealEDFReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 685039C6BD8E44625F389F7F /* RealEDFReader.swift */; }; - F2D21BB2907F3848546FB912 /* EDFReadIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEFD278699AD177C2EF54A4 /* EDFReadIntegrationTests.swift */; }; - F2D685683E4CE4752C8A335B /* SignalProcessingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05107E923F2EC062933663BB /* SignalProcessingTests.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - 05107E923F2EC062933663BB /* SignalProcessingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalProcessingTests.swift; sourceTree = ""; }; - 23549711775AE252DC4F2904 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - 277E64FC6A22251D3963489E /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; - 642C7A3D6CBF2EA4D518F57E /* EDFViewerMacTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EDFViewerMacTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 685039C6BD8E44625F389F7F /* RealEDFReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealEDFReader.swift; sourceTree = ""; }; - 6BBBEE954FC91DD87D35C0F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 6F4AA165B20C7A22C4666652 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 72A5CC800534BE9F6ABF0271 /* Fixtures */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Fixtures; path = Tests/EDFViewerMacTests/Fixtures; sourceTree = SOURCE_ROOT; }; - 73D77077467CB2C00AC9729E /* SignalProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalProcessing.swift; sourceTree = ""; }; - 9BEFD278699AD177C2EF54A4 /* EDFReadIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EDFReadIntegrationTests.swift; sourceTree = ""; }; - 9EBDB47CF168549400858F67 /* EDFViewerMacApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EDFViewerMacApp.swift; sourceTree = ""; }; - A0D30423A2EA593113195FE7 /* ViewerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewerViewModelTests.swift; sourceTree = ""; }; - E38E06C1F78853D5E4E61C0D /* WaveformMinMaxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformMinMaxView.swift; sourceTree = ""; }; - EF8C31DFA559F5C09DCB5F76 /* EDFReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EDFReader.swift; sourceTree = ""; }; - F3691C97574D02C73304ACA8 /* EDFReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EDFReaderTests.swift; sourceTree = ""; }; - F74E5E248D171C4D22769FF1 /* ViewerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewerViewModel.swift; sourceTree = ""; }; - FF3952EB1E6AE31866D64165 /* EDFViewerMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EDFViewerMac.app; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXGroup section */ - 10BE9C25C0AA4690074BA6E6 /* Products */ = { - isa = PBXGroup; - children = ( - FF3952EB1E6AE31866D64165 /* EDFViewerMac.app */, - 642C7A3D6CBF2EA4D518F57E /* EDFViewerMacTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 602B4F90E92E4139B0FB63CD /* App */ = { - isa = PBXGroup; - children = ( - 9EBDB47CF168549400858F67 /* EDFViewerMacApp.swift */, - ); - path = App; - sourceTree = ""; - }; - 6212636E960242A1FBD2AB9A /* UI */ = { - isa = PBXGroup; - children = ( - 6BBBEE954FC91DD87D35C0F6 /* ContentView.swift */, - 23549711775AE252DC4F2904 /* SettingsView.swift */, - F74E5E248D171C4D22769FF1 /* ViewerViewModel.swift */, - E38E06C1F78853D5E4E61C0D /* WaveformMinMaxView.swift */, - ); - path = UI; - sourceTree = ""; - }; - 8DE7AB746B1D517FEE8A1D82 /* EDFViewerMacTests */ = { - isa = PBXGroup; - children = ( - F3691C97574D02C73304ACA8 /* EDFReaderTests.swift */, - 9BEFD278699AD177C2EF54A4 /* EDFReadIntegrationTests.swift */, - 05107E923F2EC062933663BB /* SignalProcessingTests.swift */, - A0D30423A2EA593113195FE7 /* ViewerViewModelTests.swift */, - ); - name = EDFViewerMacTests; - path = Tests/EDFViewerMacTests; - sourceTree = ""; - }; - A2F717F3F64CE410012A6F29 = { - isa = PBXGroup; - children = ( - 72A5CC800534BE9F6ABF0271 /* Fixtures */, - DD5A95D9E7628A7A44068021 /* EDFViewerMac */, - 8DE7AB746B1D517FEE8A1D82 /* EDFViewerMacTests */, - 10BE9C25C0AA4690074BA6E6 /* Products */, - ); - sourceTree = ""; - }; - DD5A95D9E7628A7A44068021 /* EDFViewerMac */ = { - isa = PBXGroup; - children = ( - 6F4AA165B20C7A22C4666652 /* Assets.xcassets */, - 602B4F90E92E4139B0FB63CD /* App */, - E0886E15B1137D6865D6261D /* Core */, - 6212636E960242A1FBD2AB9A /* UI */, - ); - name = EDFViewerMac; - path = Sources/EDFViewerMac; - sourceTree = ""; - }; - E0886E15B1137D6865D6261D /* Core */ = { - isa = PBXGroup; - children = ( - EF8C31DFA559F5C09DCB5F76 /* EDFReader.swift */, - 277E64FC6A22251D3963489E /* Models.swift */, - 685039C6BD8E44625F389F7F /* RealEDFReader.swift */, - 73D77077467CB2C00AC9729E /* SignalProcessing.swift */, - ); - path = Core; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - D24B0A7AC084FF701C6C53E1 /* EDFViewerMacTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 82646AF6A9C63BDEE710A79A /* Build configuration list for PBXNativeTarget "EDFViewerMacTests" */; - buildPhases = ( - CE7DF445F97308FBB40783D9 /* Sources */, - 71D83192CF93FA501CDD3AD1 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = EDFViewerMacTests; - packageProductDependencies = ( - ); - productName = EDFViewerMacTests; - productReference = 642C7A3D6CBF2EA4D518F57E /* EDFViewerMacTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - E9C17CA237A96BB23B8A4900 /* EDFViewerMac */ = { - isa = PBXNativeTarget; - buildConfigurationList = 6EB6653DC04993E69AF12B39 /* Build configuration list for PBXNativeTarget "EDFViewerMac" */; - buildPhases = ( - 63666D1677C8F136D90A07EE /* Sources */, - 390E59A88B470D9F115AFA7F /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = EDFViewerMac; - packageProductDependencies = ( - ); - productName = EDFViewerMac; - productReference = FF3952EB1E6AE31866D64165 /* EDFViewerMac.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 8B024E27BCB2363FE07D4324 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1500; - TargetAttributes = { - E9C17CA237A96BB23B8A4900 = { - ProvisioningStyle = Automatic; - }; - }; - }; - buildConfigurationList = 92A067BA7D1452EDA8F23285 /* Build configuration list for PBXProject "EDFViewerMac" */; - compatibilityVersion = "Xcode 14.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - Base, - en, - ); - mainGroup = A2F717F3F64CE410012A6F29; - minimizedProjectReferenceProxies = 1; - preferredProjectObjectVersion = 77; - projectDirPath = ""; - projectRoot = ""; - targets = ( - E9C17CA237A96BB23B8A4900 /* EDFViewerMac */, - D24B0A7AC084FF701C6C53E1 /* EDFViewerMacTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 390E59A88B470D9F115AFA7F /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - C5D7FF78E15B1BF29E738205 /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 71D83192CF93FA501CDD3AD1 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 0189F499CBA2FA02C568C8DF /* Assets.xcassets in Resources */, - 522181A6606FED6AD7AACD89 /* Fixtures in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 63666D1677C8F136D90A07EE /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 4F0FA489159F9E0F24EBBA9B /* ContentView.swift in Sources */, - C4626A72D53262A2C2DD3E5A /* EDFReader.swift in Sources */, - 331E6A3E3DD6153B97ACEE5E /* EDFViewerMacApp.swift in Sources */, - 79EC8ACF0C645A877E4088ED /* Models.swift in Sources */, - 0420D985CD73DAAADB1381FF /* RealEDFReader.swift in Sources */, - BC0E02DFE3122F4782538402 /* SettingsView.swift in Sources */, - 9AE0AC69672428C5C9066DA1 /* SignalProcessing.swift in Sources */, - 8C13F9FE1B2B30AE67962BD0 /* ViewerViewModel.swift in Sources */, - B7CB8881E1070FBF4BD44ED1 /* WaveformMinMaxView.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - CE7DF445F97308FBB40783D9 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A12C86EB7DB490B17FCA5669 /* ContentView.swift in Sources */, - F2D21BB2907F3848546FB912 /* EDFReadIntegrationTests.swift in Sources */, - 52C55A2071571C081BF044C8 /* EDFReader.swift in Sources */, - B6506E56AAE7144B26E32235 /* EDFReaderTests.swift in Sources */, - 38EBB60755D0D4BF96168425 /* Models.swift in Sources */, - E47C6F9427DB7F204972E201 /* RealEDFReader.swift in Sources */, - CB97548711B45BF303A56023 /* SettingsView.swift in Sources */, - 5E30C7E6FC222DD0CCF2C6CA /* SignalProcessing.swift in Sources */, - F2D685683E4CE4752C8A335B /* SignalProcessingTests.swift in Sources */, - 4893F89CFEA376361F6BCE3F /* ViewerViewModel.swift in Sources */, - 88357D1328CDEFDFA10F3BC6 /* ViewerViewModelTests.swift in Sources */, - 8C24A0BC681D828230745D92 /* WaveformMinMaxView.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 02E1FD94EBE991D8C10F158F /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; - ENABLE_HARDENED_RUNTIME = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_GENERATION_MODE = GeneratedFile; - INFOPLIST_KEY_CFBundleDisplayName = "EDF Viewer"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.medical"; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.edfviewer.EDFViewerMac; - PRODUCT_MODULE_NAME = EDFViewerMac; - PRODUCT_NAME = "EDF Viewer"; - SDKROOT = macosx; - }; - name = Debug; - }; - 09197CDB005B109210CAA7C7 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - COMBINE_HIDPI_IMAGES = YES; - GENERATE_INFOPLIST_FILE = YES; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.edfviewer.EDFViewerMacTests; - PRODUCT_MODULE_NAME = EDFViewerMac; - SDKROOT = macosx; - }; - name = Release; - }; - 69798B97A10D34627630E915 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - COMBINE_HIDPI_IMAGES = YES; - GENERATE_INFOPLIST_FILE = YES; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.edfviewer.EDFViewerMacTests; - PRODUCT_MODULE_NAME = EDFViewerMac; - SDKROOT = macosx; - }; - name = Debug; - }; - 8FE3420A784B327A87AE4C5E /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "$(inherited)", - "DEBUG=1", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.9; - }; - name = Debug; - }; - BCB2E36216767DBDC006BCAC /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.9; - }; - name = Release; - }; - BE95C49F31585BF90D08217F /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; - ENABLE_HARDENED_RUNTIME = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_GENERATION_MODE = GeneratedFile; - INFOPLIST_KEY_CFBundleDisplayName = "EDF Viewer"; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.medical"; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.edfviewer.EDFViewerMac; - PRODUCT_MODULE_NAME = EDFViewerMac; - PRODUCT_NAME = "EDF Viewer"; - SDKROOT = macosx; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 6EB6653DC04993E69AF12B39 /* Build configuration list for PBXNativeTarget "EDFViewerMac" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 02E1FD94EBE991D8C10F158F /* Debug */, - BE95C49F31585BF90D08217F /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Debug; - }; - 82646AF6A9C63BDEE710A79A /* Build configuration list for PBXNativeTarget "EDFViewerMacTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 69798B97A10D34627630E915 /* Debug */, - 09197CDB005B109210CAA7C7 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Debug; - }; - 92A067BA7D1452EDA8F23285 /* Build configuration list for PBXProject "EDFViewerMac" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 8FE3420A784B327A87AE4C5E /* Debug */, - BCB2E36216767DBDC006BCAC /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Debug; - }; -/* End XCConfigurationList section */ - }; - rootObject = 8B024E27BCB2363FE07D4324 /* Project object */; -} diff --git a/EDFViewerMac.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/EDFViewerMac.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/EDFViewerMac.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/README.md b/README.md index 5afead7..dffaa94 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ A native macOS SwiftUI viewer for EDF/BDF biomedical signal files — free, open --- +## App Preview + +![EDF Viewer main screen](docs/images/app-main.png) + +--- + ## Why This Exists [EDFbrowser](https://www.teuniz.net/edfbrowser/) is a widely used open source EDF viewer but requires Qt and does not feel native on macOS. This project brings EDF/BDF viewing to macOS as a first-class native app — no Java, no Wine, no Qt. Just Swift and SwiftUI. @@ -141,6 +147,82 @@ xcodebuild test -project EDFViewerMac.xcodeproj -scheme EDFViewerMac --- +## Release DMG (Developer ID + Notarization) + +This repo includes XcodeGen signing config files and a release script: + +- `configs/Debug.xcconfig` +- `configs/Release.xcconfig` +- `scripts/release/release_dmg.sh` + +Required environment variables: + +```bash +export APPLE_TEAM_ID="YOUR_TEAM_ID" +export APPLE_ID="your-apple-id@example.com" +export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx" +``` + +Build, sign, notarize, and staple a DMG: + +```bash +./scripts/release/release_dmg.sh +``` + +Output: + +- `build/release/EDFViewer.dmg` + +### GitHub Actions release (signed DMG) + +The workflow at `.github/workflows/release.yml` will: + +- build an unsigned DMG if signing secrets are missing +- build, sign, notarize, and staple the DMG if secrets are present + +Add these repository secrets: + +- `APPLE_TEAM_ID` +- `APPLE_ID` +- `APPLE_APP_SPECIFIC_PASSWORD` +- `MACOS_CERT_P12_BASE64` +- `MACOS_CERT_PASSWORD` +- `MACOS_KEYCHAIN_PASSWORD` (optional, recommended) + +Create `MACOS_CERT_P12_BASE64` from your Developer ID certificate export: + +1. Open **Keychain Access** +2. Export your **Developer ID Application** cert + private key as `.p12` +3. Convert to base64: + +```bash +base64 -i developer_id_application.p12 | pbcopy +``` + +Paste that copied value into `MACOS_CERT_P12_BASE64`. + +Flow: + +- `develop` branch pushes -> temporary DMG workflow (`release-temp-dmg.yml`) +- `main` tags (`v*`) -> signed + notarized release workflow (`release.yml`) + +### Temporary DMG pipeline (no Apple notarization) + +Use workflow: + +- `.github/workflows/release-temp-dmg.yml` + +How to run: + +1. GitHub -> **Actions** +2. Choose **Release Temp DMG** (or push to `develop` to run automatically) +3. Click **Run workflow** +4. Download `temp-dmg` artifact + +This build is ad-hoc signed for testing only and may show Gatekeeper warnings on other Macs. + +--- + ## Roadmap ### Done — Viewer Foundation @@ -196,3 +278,9 @@ MIT License. See [LICENSE](LICENSE) for details. *"Heal the sick, cleanse the lepers, raise the dead, cast out devils: freely ye have received, freely give."* — Matthew 10:8 + +--- + +## Sponsor + +[![Sponsor](https://img.shields.io/badge/Sponsor-zywkloo-ea4aaa?logo=githubsponsors)](https://github.com/sponsors/zywkloo) diff --git a/Sources/EDFViewerMac/UI/ContentView.swift b/Sources/EDFViewerMac/UI/ContentView.swift index b3ec18c..2cf43fa 100644 --- a/Sources/EDFViewerMac/UI/ContentView.swift +++ b/Sources/EDFViewerMac/UI/ContentView.swift @@ -47,6 +47,8 @@ struct ContentView: View { .foregroundStyle(.secondary) } + allChannelsRow + List(selection: Binding( get: { viewModel.allChannelsMode ? nil : viewModel.selectedChannelID }, set: { newValue in @@ -54,25 +56,6 @@ struct ContentView: View { Task { await viewModel.selectChannel(id, pixelWidth: 1400) } }) ) { - Button { - Task { await viewModel.selectAllChannels(pixelWidth: 1400) } - } label: { - VStack(alignment: .leading, spacing: 2) { - Text("All Channels") - .font(.body.bold()) - .foregroundColor(.accentColor) - if viewModel.fileDurationSeconds > 0 { - Text(formatDuration(viewModel.fileDurationSeconds)) - .font(.caption) - .foregroundStyle(.secondary) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.vertical, 4) - } - .buttonStyle(.plain) - .listRowBackground(viewModel.allChannelsMode ? Color.accentColor.opacity(0.2) : Color.clear) - ForEach(viewModel.channels) { channel in VStack(alignment: .leading, spacing: 2) { Text(channel.label) @@ -91,6 +74,34 @@ struct ContentView: View { .padding(12) } + private var allChannelsRow: some View { + VStack(alignment: .leading, spacing: 2) { + Text("All Channels") + .font(.body.bold()) + .foregroundStyle(viewModel.allChannelsMode ? Color.accentColor : .primary) + if viewModel.fileDurationSeconds > 0 { + Text(formatDuration(viewModel.fileDurationSeconds)) + .font(.caption) + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 12) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(viewModel.allChannelsMode ? Color.accentColor.opacity(0.2) : Color(NSColor.controlBackgroundColor)) + ) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(viewModel.allChannelsMode ? Color.accentColor.opacity(0.5) : Color.gray.opacity(0.2), lineWidth: 1) + ) + .contentShape(Rectangle()) + .onTapGesture { + Task { await viewModel.selectAllChannels(pixelWidth: 1400) } + } + } + private var waveformPane: some View { VStack(spacing: 8) { HStack { diff --git a/app-main.png b/app-main.png new file mode 100644 index 0000000..1dda35b Binary files /dev/null and b/app-main.png differ diff --git a/configs/Debug.xcconfig b/configs/Debug.xcconfig new file mode 100644 index 0000000..7b54947 --- /dev/null +++ b/configs/Debug.xcconfig @@ -0,0 +1,3 @@ +CODE_SIGN_STYLE = Automatic +ENABLE_HARDENED_RUNTIME = YES +ENABLE_DEBUG_DYLIB = NO diff --git a/configs/Release.xcconfig b/configs/Release.xcconfig new file mode 100644 index 0000000..ea4478b --- /dev/null +++ b/configs/Release.xcconfig @@ -0,0 +1,11 @@ +CODE_SIGN_STYLE = Manual + +// Pass APPLE_TEAM_ID at build time: xcodebuild ... APPLE_TEAM_ID=YOURTEAMID +DEVELOPMENT_TEAM = $(APPLE_TEAM_ID) +CODE_SIGN_IDENTITY = Developer ID Application +CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO +ENABLE_HARDENED_RUNTIME = YES +ENABLE_DEBUG_DYLIB = NO + +// Keep timestamps for Gatekeeper and notarization checks. +OTHER_CODE_SIGN_FLAGS = --timestamp diff --git a/docs/images/app-main.png b/docs/images/app-main.png new file mode 100644 index 0000000..1dda35b Binary files /dev/null and b/docs/images/app-main.png differ diff --git a/project.yml b/project.yml index ae056cf..f5cbb4f 100644 --- a/project.yml +++ b/project.yml @@ -1,6 +1,6 @@ name: EDFViewerMac options: - bundleIdPrefix: com.edfviewer + bundleIdPrefix: com.goldenarc deploymentTarget: macOS: "13.0" xcodeVersion: "15.0" @@ -13,14 +13,18 @@ targets: EDFViewerMac: type: application platform: macOS + configFiles: + Debug: configs/Debug.xcconfig + Release: configs/Release.xcconfig sources: - path: Sources/EDFViewerMac excludes: - "**/.DS_Store" settings: base: - PRODUCT_BUNDLE_IDENTIFIER: com.edfviewer.EDFViewerMac - PRODUCT_NAME: "EDF Viewer" + PRODUCT_BUNDLE_IDENTIFIER: com.goldenarc.edfviewer + PRODUCT_NAME: EDFViewer + EXECUTABLE_NAME: EDFViewer PRODUCT_MODULE_NAME: EDFViewerMac INFOPLIST_GENERATION_MODE: GeneratedFile MARKETING_VERSION: "1.0.0" @@ -31,6 +35,7 @@ targets: INFOPLIST_KEY_NSHumanReadableCopyright: "" CODE_SIGN_STYLE: Automatic ENABLE_HARDENED_RUNTIME: YES + ENABLE_DEBUG_DYLIB: NO COMBINE_HIDPI_IMAGES: YES EDFViewerMacTests: type: bundle.unit-test @@ -49,6 +54,6 @@ targets: buildPhase: resources settings: base: - PRODUCT_BUNDLE_IDENTIFIER: com.edfviewer.EDFViewerMacTests + PRODUCT_BUNDLE_IDENTIFIER: com.goldenarc.edfviewer.tests GENERATE_INFOPLIST_FILE: YES PRODUCT_MODULE_NAME: EDFViewerMac diff --git a/scripts/release/release_dmg.sh b/scripts/release/release_dmg.sh new file mode 100755 index 0000000..20a3e46 --- /dev/null +++ b/scripts/release/release_dmg.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Required environment variables: +# APPLE_TEAM_ID e.g. 55X48AGS79 +# APPLE_ID Apple ID email used for notarization +# APPLE_APP_SPECIFIC_PASSWORD App-specific password (or use notarytool keychain profile) + +if [[ -z "${APPLE_TEAM_ID:-}" || -z "${APPLE_ID:-}" || -z "${APPLE_APP_SPECIFIC_PASSWORD:-}" ]]; then + echo "Missing required env vars." + echo "Set APPLE_TEAM_ID, APPLE_ID, and APPLE_APP_SPECIFIC_PASSWORD." + exit 1 +fi + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +PROJECT="$ROOT_DIR/EDFViewerMac.xcodeproj" +SCHEME="EDFViewerMac" +CONFIG="Release" +APP_NAME="EDFViewer" +DISPLAY_NAME="EDF Viewer" +OUT_DIR="$ROOT_DIR/build/release" +ARCHIVE_PATH="$OUT_DIR/$APP_NAME.xcarchive" +EXPORT_DIR="$OUT_DIR/export" +APP_PATH="$EXPORT_DIR/$APP_NAME.app" +DMG_PATH="$OUT_DIR/$APP_NAME.dmg" +EXPORT_PLIST="$OUT_DIR/ExportOptions.plist" + +mkdir -p "$OUT_DIR" + +cd "$ROOT_DIR" +xcodegen generate + +cat > "$EXPORT_PLIST" < + + + + method + developer-id + signingStyle + manual + teamID + ${APPLE_TEAM_ID} + + +PLIST + +xcodebuild archive \ + -project "$PROJECT" \ + -scheme "$SCHEME" \ + -configuration "$CONFIG" \ + -archivePath "$ARCHIVE_PATH" \ + APPLE_TEAM_ID="$APPLE_TEAM_ID" + +xcodebuild -exportArchive \ + -archivePath "$ARCHIVE_PATH" \ + -exportPath "$EXPORT_DIR" \ + -exportOptionsPlist "$EXPORT_PLIST" + +if [[ ! -d "$APP_PATH" ]]; then + echo "Expected app not found at $APP_PATH" + exit 1 +fi + +hdiutil create \ + -volname "$DISPLAY_NAME" \ + -srcfolder "$APP_PATH" \ + -ov -format UDZO \ + "$DMG_PATH" + +codesign --force --sign "Developer ID Application" --timestamp "$DMG_PATH" + +xcrun notarytool submit "$DMG_PATH" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + +xcrun stapler staple "$DMG_PATH" +spctl -a -t open --context context:primary-signature -v "$DMG_PATH" + +echo "Release DMG ready: $DMG_PATH"