From 8d4638d65c925c34302e0d4e5a4f66f8f9149bb3 Mon Sep 17 00:00:00 2001 From: Josh <46817760+joshedney@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:31:47 +0000 Subject: [PATCH 1/6] v2.8.0 Release (#159) * rename upload dsym to xcode build * add changelog entry * rename dsym functions * add xcarchive command * add utils for dealing with xcarchives * deprecate the dsym command * fix cli options * update changelog * Update README.md Co-authored-by: Tom Longridge * Update README.md Co-authored-by: Tom Longridge * re-enable swift package manager tests * rename dsym tests to xcode * rename dsym tests to xcode * add switf package manager fixture * update swift package manager test * rmeove swift-package-manager * Update CHANGELOG.md Co-authored-by: Tom Longridge * Update pkg/ios/xcodebuild-utils.go Co-authored-by: Tom Longridge * Update pkg/upload/xcode-archive.go Co-authored-by: Tom Longridge * update what folders we check for xcarchives when dealing with xcodeprojects * update submodules * update submodules * update submodules * remove FindLatestFolder as it is unused * add seperate function to deal with processing dsyms; * adjust what we send to the process dsym command * updatge unity version * update comments on functions that have been touched * adjust variable init * add tests for xcarchive * Update CHANGELOG.md Co-authored-by: Tom Longridge * fix dsym util filewalk error * fix dsym util filewalk error * fix dsym util filewalk error * update xcarchive functions with comments * update unity android tests * update xcarchive test * Update pkg/ios/plist-reader-json.go Co-authored-by: Tom Longridge * Update pkg/ios/plist-reader-json.go Co-authored-by: Tom Longridge * combine a few xcarxchive functions together and move some checks to lower functions * update how we reference xcode archive * share xcode options between both * Update pkg/ios/xcarchive.go Co-authored-by: Tom Longridge * go mod tidy * update readme with xcode archive * remove alias for dsym on xcode build * update label for xcode tests * add date to change log * update change log description * skip RN 0.69 ios tests * skip RN 0.69 ios tests --------- Co-authored-by: Tom Longridge --- .buildkite/pipeline.yml | 7 +- CHANGELOG.md | 5 + Gemfile | 2 +- README.md | 12 +- .../{unity-2023.feature => unity.feature} | 4 +- .../expected_err_and_warn_scenarios.feature | 25 --- features/{dsym => xcode}/dsym_upload.feature | 21 +-- .../expected_err_and_warn_scenarios.feature | 25 +++ .../MissingDWARF.dSYM/Contents/Info.plist | 0 .../fixtures/ZeroByteDsym/ZeroByteFile.dSYM | 0 .../fixtures/app-center/symbols.zip | Bin .../Info.plist" | 0 .../Contents/Info.plist" | 0 .../Contents/Resources/DWARF/bugsnag-example" | Bin features/{dsym => xcode}/fixtures/dsyms.zip | Bin .../app.dSYM/Contents/Resources/DWARF/app | Bin .../app2.dSYM/Contents/Resources/DWARF/app | Bin .../fixtures/fl-project/Gemfile | 0 .../MissingDWARF.dSYM/Contents/Info.plist | 0 .../fixtures/fl-project/NoApiKey.plist | 0 .../fixtures/fl-project/TestList.plist | 0 .../fl-project/ZeroByteDsym/ZeroByteFile.dSYM | 0 .../fixtures/fl-project/dsym2.zip | Bin .../fixtures/fl-project/dsym3.zip | Bin .../fixtures/fl-project/dsyms.zip | Bin .../app.dSYM/Contents/Resources/DWARF/app | Bin .../app2.dSYM/Contents/Resources/DWARF/app | Bin .../fixtures/fl-project/fastlane/Fastfile | 0 .../some files \316\262.app.dSYM.zip" | Bin .../fixtures/macos-compressed-dsyms.zip | Bin .../{dsym => xcode}/fixtures/single-dsym.zip | Bin .../app.dSYM/Contents/Resources/DWARF/app | Bin .../Contents/Info.plist | 0 .../Resources/DWARF/swift-package-manager | Bin .../aarch64/swift-package-manager.yml | 0 .../swift_package_manager.feature | 25 ++- features/xcode/xcarchive.feature | 18 ++ go.mod | 6 +- go.sum | 16 +- main.go | 20 ++- makefile | 13 +- pkg/ios/dsym-utils.go | 75 +++++--- pkg/ios/plist-reader-json.go | 68 +++++--- pkg/ios/process-dsym.go | 78 +++++++++ pkg/ios/xcarchive.go | 99 +++++++++++ pkg/ios/xcodebuild-utils.go | 156 ++++++++++------- pkg/options/options.go | 33 ++-- pkg/server/request.go | 2 +- pkg/upload/dsym.go | 162 ------------------ pkg/upload/react-native-ios.go | 3 +- pkg/upload/react-native.go | 56 +++--- pkg/upload/xcode-archive.go | 97 +++++++++++ pkg/upload/xcode-build.go | 130 ++++++++++++++ pkg/utils/files.go | 137 ++++++++++----- 54 files changed, 873 insertions(+), 422 deletions(-) rename features/Unity-Android/{unity-2023.feature => unity.feature} (95%) delete mode 100644 features/dsym/expected_err_and_warn_scenarios.feature rename features/{dsym => xcode}/dsym_upload.feature (54%) create mode 100644 features/xcode/expected_err_and_warn_scenarios.feature rename features/{dsym => xcode}/fixtures/MissingDWARFdSYM/MissingDWARF.dSYM/Contents/Info.plist (100%) rename features/{dsym => xcode}/fixtures/ZeroByteDsym/ZeroByteFile.dSYM (100%) rename features/{dsym => xcode}/fixtures/app-center/symbols.zip (100%) rename "features/dsym/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/Info.plist" => "features/xcode/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/Info.plist" (100%) rename "features/dsym/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/dSYMs/bugsnag-example.app.dSYM/Contents/Info.plist" => "features/xcode/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/dSYMs/bugsnag-example.app.dSYM/Contents/Info.plist" (100%) rename "features/dsym/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/dSYMs/bugsnag-example.app.dSYM/Contents/Resources/DWARF/bugsnag-example" => "features/xcode/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/dSYMs/bugsnag-example.app.dSYM/Contents/Resources/DWARF/bugsnag-example" (100%) rename features/{dsym => xcode}/fixtures/dsyms.zip (100%) rename features/{dsym => xcode}/fixtures/dsyms/app.dSYM/Contents/Resources/DWARF/app (100%) rename features/{dsym => xcode}/fixtures/dsyms/app2.dSYM/Contents/Resources/DWARF/app (100%) rename features/{dsym => xcode}/fixtures/fl-project/Gemfile (100%) rename features/{dsym => xcode}/fixtures/fl-project/MissingDWARFdSYM/MissingDWARF.dSYM/Contents/Info.plist (100%) rename features/{dsym => xcode}/fixtures/fl-project/NoApiKey.plist (100%) rename features/{dsym => xcode}/fixtures/fl-project/TestList.plist (100%) rename features/{dsym => xcode}/fixtures/fl-project/ZeroByteDsym/ZeroByteFile.dSYM (100%) rename features/{dsym => xcode}/fixtures/fl-project/dsym2.zip (100%) rename features/{dsym => xcode}/fixtures/fl-project/dsym3.zip (100%) rename features/{dsym => xcode}/fixtures/fl-project/dsyms.zip (100%) rename features/{dsym => xcode}/fixtures/fl-project/dsyms/app.dSYM/Contents/Resources/DWARF/app (100%) rename features/{dsym => xcode}/fixtures/fl-project/dsyms/app2.dSYM/Contents/Resources/DWARF/app (100%) rename features/{dsym => xcode}/fixtures/fl-project/fastlane/Fastfile (100%) rename "features/dsym/fixtures/fl-project/some dir/some files \316\262.app.dSYM.zip" => "features/xcode/fixtures/fl-project/some dir/some files \316\262.app.dSYM.zip" (100%) rename features/{dsym => xcode}/fixtures/macos-compressed-dsyms.zip (100%) rename features/{dsym => xcode}/fixtures/single-dsym.zip (100%) rename features/{dsym => xcode}/fixtures/single-dsym/app.dSYM/Contents/Resources/DWARF/app (100%) rename features/{dsym => xcode}/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Info.plist (100%) rename features/{dsym => xcode}/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Resources/DWARF/swift-package-manager (100%) rename features/{dsym => xcode}/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Resources/Relocations/aarch64/swift-package-manager.yml (100%) rename features/{dsym => xcode}/swift_package_manager.feature (51%) create mode 100644 features/xcode/xcarchive.feature create mode 100644 pkg/ios/process-dsym.go create mode 100644 pkg/ios/xcarchive.go delete mode 100644 pkg/upload/dsym.go create mode 100644 pkg/upload/xcode-archive.go create mode 100644 pkg/upload/xcode-build.go diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index b4ba94fb..4dcb3ad8 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -70,14 +70,14 @@ steps: upload: - maze_output/**/* - - label: dSYM Integration Tests + - label: Xcode Integration Tests depends_on: build env: XCODE_VERSION: 15 commands: - bundle install - chmod +x bin/arm64-macos-bugsnag-cli - - bundle exec maze-runner features/dsym + - bundle exec maze-runner features/xcode plugins: artifacts#v1.5.0: download: @@ -101,7 +101,7 @@ steps: - label: ":video_game: Unity Android Integration Tests" depends_on: build env: - UNITY_VERSION: 2023.2.19f1 + UNITY_VERSION: 6000.0.25f1 commands: - chmod +x bin/arm64-macos-bugsnag-cli - bundle install @@ -116,6 +116,7 @@ steps: - group: ":react: React Native" steps: - label: "RN 0.69 :ios: Integration Tests" + skip: "issue with Boost https://github.com/facebook/react-native/issues/42180" depends_on: build agents: queue: macos-12-arm diff --git a/CHANGELOG.md b/CHANGELOG.md index cabbd1a4..dbe68d86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Changelog +## 2.8.0 (2025-01-06) + +### Enhancements +- Add the Xcode Archive Command to support the uploading of xcarchive files [156](https://github.com/bugsnag/bugsnag-cli/pull/156) +- Rename the `dsym` upload command to `xcode-build` to better reflect the command's purpose. `dsym` will be removed in the next major release [156](https://github.com/bugsnag/bugsnag-cli/pull/156) ## 2.7.0 (2024-11-26) diff --git a/Gemfile b/Gemfile index 9a5151e1..69dc7b7a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" -gem "bugsnag-maze-runner", git: "https://github.com/bugsnag/maze-runner", tag: 'v9.11.2' +gem "bugsnag-maze-runner", git: "https://github.com/bugsnag/maze-runner", tag: "v9.21.0" gem 'cocoapods' diff --git a/README.md b/README.md index c81c9054..48d0f5ef 100644 --- a/README.md +++ b/README.md @@ -104,11 +104,17 @@ If you are stripping debug symbols from your Dart code when building your Flutte $ bugsnag-cli upload dart --api-key=YOUR_API_KEY app-debug-info/ -### dSYM files (iOS, macOS, tvOS) +### dSYM files from an Xcode build (iOS, macOS, tvOS) -Upload dSYM files to allow BugSnag to show human-friendly function names, file paths, and line numbers in your iOS, macOS, and tvOS stacktraces. +Upload dSYM files generated from a Build in Xcode to allow BugSnag to show human-friendly function names, file paths, and line numbers in your iOS, macOS, and tvOS stacktraces. - $ bugsnag-cli upload dsym + $ bugsnag-cli upload xcode-build + +### dSYM files from an Xcode Archive (iOS, macOS, tvOS) + +Upload dSYM files generated from an Archive in Xcode to allow BugSnag to show human-friendly function names, file paths, and line numbers in your iOS, macOS, and tvOS stacktraces. + + $ bugsnag-cli upload xcode-archive ### Unity Symbol Files (Android only) diff --git a/features/Unity-Android/unity-2023.feature b/features/Unity-Android/unity.feature similarity index 95% rename from features/Unity-Android/unity-2023.feature rename to features/Unity-Android/unity.feature index d7ad9afd..807dc230 100644 --- a/features/Unity-Android/unity-2023.feature +++ b/features/Unity-Android/unity.feature @@ -4,7 +4,7 @@ Feature: Unity Android integration tests And I wait for the Unity symbols to generate When I run bugsnag-cli with upload unity-android --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --overwrite platforms-examples/Unity/ - Then I wait to receive 4 sourcemaps + Then I wait to receive 5 sourcemaps Then the sourcemap is valid for the Android Build API Then the sourcemaps Content-Type header is valid multipart form-data And the sourcemap payload field "apiKey" equals "1234567890ABCDEF1234567890ABCDEF" @@ -19,7 +19,7 @@ Feature: Unity Android integration tests And I wait for the Unity symbols to generate When I run bugsnag-cli with upload unity-android --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --overwrite --aab-path platforms-examples/Unity/UnityExample.aab platforms-examples/Unity/ - Then I wait to receive 4 sourcemaps + Then I wait to receive 5 sourcemaps Then the sourcemap is valid for the Android Build API Then the sourcemaps Content-Type header is valid multipart form-data And the sourcemap payload field "apiKey" equals "1234567890ABCDEF1234567890ABCDEF" diff --git a/features/dsym/expected_err_and_warn_scenarios.feature b/features/dsym/expected_err_and_warn_scenarios.feature deleted file mode 100644 index 1514db5c..00000000 --- a/features/dsym/expected_err_and_warn_scenarios.feature +++ /dev/null @@ -1,25 +0,0 @@ -Feature: dSYM Expected Error and Warning scenario Integration Tests - - Scenario: If --ignore-empty-dsym is set to true, then the log message returned should be [WARN] - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/path/to/project/root --scheme=test --ignore-empty-dsym=true features/dsym/fixtures/ZeroByteDsym - Then I should see a log level of "[FATAL]" when no dSYM files could be found - - Scenario: If --ignore-empty-dsym is not set, then the log message returned should be [ERROR] - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/path/to/project/root --scheme=test features/dsym/fixtures/ZeroByteDsym - Then I should see a log level of "[FATAL]" when no dSYM files could be found - - Scenario: If --ignore-missing-dwarf is set to true, then the log message returned should be [WARN] - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/path/to/project/root --scheme=test --ignore-missing-dwarf=true features/dsym/fixtures/MissingDWARFdSYM - Then I should see a log level of "[FATAL]" when no dSYM files could be found - - Scenario: If --ignore-missing-dwarf is not set, then the log message returned should be [ERROR] - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/path/to/project/root --scheme=test features/dsym/fixtures/MissingDWARFdSYM - Then I should see a log level of "[FATAL]" when no dSYM files could be found - - Scenario: If --ignore-missing-dwarf is set to true, then the log message returned should be [WARN] - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/path/to/project/root --scheme=test --ignore-missing-dwarf=true features/dsym/fixtures/MissingDWARFdSYM - Then I should see a log level of "[FATAL]" when no dSYM files could be found - - Scenario: If --ignore-missing-dwarf is not set, then the log message returned should be [ERROR] - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/path/to/project/root --scheme=test features/dsym/fixtures/MissingDWARFdSYM - Then I should see a log level of "[FATAL]" when no dSYM files could be found diff --git a/features/dsym/dsym_upload.feature b/features/xcode/dsym_upload.feature similarity index 54% rename from features/dsym/dsym_upload.feature rename to features/xcode/dsym_upload.feature index b00e7111..6c535de9 100644 --- a/features/dsym/dsym_upload.feature +++ b/features/xcode/dsym_upload.feature @@ -1,49 +1,42 @@ Feature: dSYM Upload Integration Tests Scenario: Upload a single dSYM sourcemap using path containing one dSYM - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ features/dsym/fixtures/single-dsym + When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ features/xcode/fixtures/single-dsym And I wait to receive 1 sourcemaps Then the sourcemap is valid for the dSYM Build API Then the sourcemaps Content-Type header is valid multipart form-data And the sourcemap payload field "apiKey" equals "1234567890ABCDEF1234567890ABCDEF" Scenario: Upload multiple dSYM sourcemaps using path containing multiple dSYMs - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ features/dsym/fixtures/dsyms + When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ features/xcode/fixtures/dsyms And I wait to receive 2 sourcemaps Then the sourcemap is valid for the dSYM Build API Then the sourcemaps Content-Type header is valid multipart form-data And the sourcemap payload field "apiKey" equals "1234567890ABCDEF1234567890ABCDEF" Scenario: Upload a single dSYM sourcemap using zip file containing one dSYM - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ features/dsym/fixtures/single-dsym.zip + When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ features/xcode/fixtures/single-dsym.zip And I wait to receive 1 sourcemaps Then the sourcemap is valid for the dSYM Build API Then the sourcemaps Content-Type header is valid multipart form-data And the sourcemap payload field "apiKey" equals "1234567890ABCDEF1234567890ABCDEF" Scenario: Upload multiple dSYM sourcemaps using zip file containing multiple dSYMs - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ features/dsym/fixtures/dsyms.zip + When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ features/xcode/fixtures/dsyms.zip And I wait to receive 2 sourcemaps Then the sourcemap is valid for the dSYM Build API Then the sourcemaps Content-Type header is valid multipart form-data And the sourcemap payload field "apiKey" equals "1234567890ABCDEF1234567890ABCDEF" Scenario: Uploading a zip file containing directory of dSYM files that was compressed with macOS Archive Utility - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ features/dsym/fixtures/macos-compressed-dsyms.zip + When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ features/xcode/fixtures/macos-compressed-dsyms.zip And I wait to receive 2 sourcemaps Then the sourcemap is valid for the dSYM Build API Then the sourcemaps Content-Type header is valid multipart form-data And the sourcemap payload field "apiKey" equals "1234567890ABCDEF1234567890ABCDEF" Scenario: Upload symbols from an AppCenter zip - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ "features/dsym/fixtures/app-center/symbols.zip" - And I wait to receive 2 sourcemaps - Then the sourcemap is valid for the dSYM Build API - Then the sourcemaps Content-Type header is valid multipart form-data - And the sourcemap payload field "apiKey" equals "1234567890ABCDEF1234567890ABCDEF" - - Scenario: Uploading an .xcarchive containing commas and special characters - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ "features/dsym/fixtures/bugsnag-example 14-05-2021,,, 11.27éøœåñü#.xcarchive" + When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ "features/xcode/fixtures/app-center/symbols.zip" And I wait to receive 2 sourcemaps Then the sourcemap is valid for the dSYM Build API Then the sourcemaps Content-Type header is valid multipart form-data @@ -53,7 +46,7 @@ Feature: dSYM Upload Integration Tests When I make the "features/base-fixtures/dsym" Then I wait for the build to succeed - When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ features/base-fixtures/dsym/ + When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/my/project/root/ features/base-fixtures/dsym/ And I wait to receive 1 sourcemaps Then the sourcemap is valid for the dSYM Build API Then the sourcemaps Content-Type header is valid multipart form-data diff --git a/features/xcode/expected_err_and_warn_scenarios.feature b/features/xcode/expected_err_and_warn_scenarios.feature new file mode 100644 index 00000000..7797a3d4 --- /dev/null +++ b/features/xcode/expected_err_and_warn_scenarios.feature @@ -0,0 +1,25 @@ +Feature: dSYM Expected Error and Warning scenario Integration Tests + + Scenario: If --ignore-empty-dsym is set to true, then the log message returned should be [WARN] + When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/path/to/project/root --scheme=test --ignore-empty-dsym=true features/xcode/fixtures/ZeroByteDsym + Then I should see a log level of "[FATAL]" when no dSYM files could be found + + Scenario: If --ignore-empty-dsym is not set, then the log message returned should be [ERROR] + When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/path/to/project/root --scheme=test features/xcode/fixtures/ZeroByteDsym + Then I should see a log level of "[FATAL]" when no dSYM files could be found + + Scenario: If --ignore-missing-dwarf is set to true, then the log message returned should be [WARN] + When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/path/to/project/root --scheme=test --ignore-missing-dwarf=true features/xcode/fixtures/MissingDWARFdSYM + Then I should see a log level of "[FATAL]" when no dSYM files could be found + + Scenario: If --ignore-missing-dwarf is not set, then the log message returned should be [ERROR] + When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/path/to/project/root --scheme=test features/xcode/fixtures/MissingDWARFdSYM + Then I should see a log level of "[FATAL]" when no dSYM files could be found + + Scenario: If --ignore-missing-dwarf is set to true, then the log message returned should be [WARN] + When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/path/to/project/root --scheme=test --ignore-missing-dwarf=true features/xcode/fixtures/MissingDWARFdSYM + Then I should see a log level of "[FATAL]" when no dSYM files could be found + + Scenario: If --ignore-missing-dwarf is not set, then the log message returned should be [ERROR] + When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --project-root=/path/to/project/root --scheme=test features/xcode/fixtures/MissingDWARFdSYM + Then I should see a log level of "[FATAL]" when no dSYM files could be found diff --git a/features/dsym/fixtures/MissingDWARFdSYM/MissingDWARF.dSYM/Contents/Info.plist b/features/xcode/fixtures/MissingDWARFdSYM/MissingDWARF.dSYM/Contents/Info.plist similarity index 100% rename from features/dsym/fixtures/MissingDWARFdSYM/MissingDWARF.dSYM/Contents/Info.plist rename to features/xcode/fixtures/MissingDWARFdSYM/MissingDWARF.dSYM/Contents/Info.plist diff --git a/features/dsym/fixtures/ZeroByteDsym/ZeroByteFile.dSYM b/features/xcode/fixtures/ZeroByteDsym/ZeroByteFile.dSYM similarity index 100% rename from features/dsym/fixtures/ZeroByteDsym/ZeroByteFile.dSYM rename to features/xcode/fixtures/ZeroByteDsym/ZeroByteFile.dSYM diff --git a/features/dsym/fixtures/app-center/symbols.zip b/features/xcode/fixtures/app-center/symbols.zip similarity index 100% rename from features/dsym/fixtures/app-center/symbols.zip rename to features/xcode/fixtures/app-center/symbols.zip diff --git "a/features/dsym/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/Info.plist" "b/features/xcode/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/Info.plist" similarity index 100% rename from "features/dsym/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/Info.plist" rename to "features/xcode/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/Info.plist" diff --git "a/features/dsym/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/dSYMs/bugsnag-example.app.dSYM/Contents/Info.plist" "b/features/xcode/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/dSYMs/bugsnag-example.app.dSYM/Contents/Info.plist" similarity index 100% rename from "features/dsym/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/dSYMs/bugsnag-example.app.dSYM/Contents/Info.plist" rename to "features/xcode/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/dSYMs/bugsnag-example.app.dSYM/Contents/Info.plist" diff --git "a/features/dsym/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/dSYMs/bugsnag-example.app.dSYM/Contents/Resources/DWARF/bugsnag-example" "b/features/xcode/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/dSYMs/bugsnag-example.app.dSYM/Contents/Resources/DWARF/bugsnag-example" similarity index 100% rename from "features/dsym/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/dSYMs/bugsnag-example.app.dSYM/Contents/Resources/DWARF/bugsnag-example" rename to "features/xcode/fixtures/bugsnag-example 14-05-2021,,, 11.27\303\251\303\270\305\223\303\245\303\261\303\274#.xcarchive/dSYMs/bugsnag-example.app.dSYM/Contents/Resources/DWARF/bugsnag-example" diff --git a/features/dsym/fixtures/dsyms.zip b/features/xcode/fixtures/dsyms.zip similarity index 100% rename from features/dsym/fixtures/dsyms.zip rename to features/xcode/fixtures/dsyms.zip diff --git a/features/dsym/fixtures/dsyms/app.dSYM/Contents/Resources/DWARF/app b/features/xcode/fixtures/dsyms/app.dSYM/Contents/Resources/DWARF/app similarity index 100% rename from features/dsym/fixtures/dsyms/app.dSYM/Contents/Resources/DWARF/app rename to features/xcode/fixtures/dsyms/app.dSYM/Contents/Resources/DWARF/app diff --git a/features/dsym/fixtures/dsyms/app2.dSYM/Contents/Resources/DWARF/app b/features/xcode/fixtures/dsyms/app2.dSYM/Contents/Resources/DWARF/app similarity index 100% rename from features/dsym/fixtures/dsyms/app2.dSYM/Contents/Resources/DWARF/app rename to features/xcode/fixtures/dsyms/app2.dSYM/Contents/Resources/DWARF/app diff --git a/features/dsym/fixtures/fl-project/Gemfile b/features/xcode/fixtures/fl-project/Gemfile similarity index 100% rename from features/dsym/fixtures/fl-project/Gemfile rename to features/xcode/fixtures/fl-project/Gemfile diff --git a/features/dsym/fixtures/fl-project/MissingDWARFdSYM/MissingDWARF.dSYM/Contents/Info.plist b/features/xcode/fixtures/fl-project/MissingDWARFdSYM/MissingDWARF.dSYM/Contents/Info.plist similarity index 100% rename from features/dsym/fixtures/fl-project/MissingDWARFdSYM/MissingDWARF.dSYM/Contents/Info.plist rename to features/xcode/fixtures/fl-project/MissingDWARFdSYM/MissingDWARF.dSYM/Contents/Info.plist diff --git a/features/dsym/fixtures/fl-project/NoApiKey.plist b/features/xcode/fixtures/fl-project/NoApiKey.plist similarity index 100% rename from features/dsym/fixtures/fl-project/NoApiKey.plist rename to features/xcode/fixtures/fl-project/NoApiKey.plist diff --git a/features/dsym/fixtures/fl-project/TestList.plist b/features/xcode/fixtures/fl-project/TestList.plist similarity index 100% rename from features/dsym/fixtures/fl-project/TestList.plist rename to features/xcode/fixtures/fl-project/TestList.plist diff --git a/features/dsym/fixtures/fl-project/ZeroByteDsym/ZeroByteFile.dSYM b/features/xcode/fixtures/fl-project/ZeroByteDsym/ZeroByteFile.dSYM similarity index 100% rename from features/dsym/fixtures/fl-project/ZeroByteDsym/ZeroByteFile.dSYM rename to features/xcode/fixtures/fl-project/ZeroByteDsym/ZeroByteFile.dSYM diff --git a/features/dsym/fixtures/fl-project/dsym2.zip b/features/xcode/fixtures/fl-project/dsym2.zip similarity index 100% rename from features/dsym/fixtures/fl-project/dsym2.zip rename to features/xcode/fixtures/fl-project/dsym2.zip diff --git a/features/dsym/fixtures/fl-project/dsym3.zip b/features/xcode/fixtures/fl-project/dsym3.zip similarity index 100% rename from features/dsym/fixtures/fl-project/dsym3.zip rename to features/xcode/fixtures/fl-project/dsym3.zip diff --git a/features/dsym/fixtures/fl-project/dsyms.zip b/features/xcode/fixtures/fl-project/dsyms.zip similarity index 100% rename from features/dsym/fixtures/fl-project/dsyms.zip rename to features/xcode/fixtures/fl-project/dsyms.zip diff --git a/features/dsym/fixtures/fl-project/dsyms/app.dSYM/Contents/Resources/DWARF/app b/features/xcode/fixtures/fl-project/dsyms/app.dSYM/Contents/Resources/DWARF/app similarity index 100% rename from features/dsym/fixtures/fl-project/dsyms/app.dSYM/Contents/Resources/DWARF/app rename to features/xcode/fixtures/fl-project/dsyms/app.dSYM/Contents/Resources/DWARF/app diff --git a/features/dsym/fixtures/fl-project/dsyms/app2.dSYM/Contents/Resources/DWARF/app b/features/xcode/fixtures/fl-project/dsyms/app2.dSYM/Contents/Resources/DWARF/app similarity index 100% rename from features/dsym/fixtures/fl-project/dsyms/app2.dSYM/Contents/Resources/DWARF/app rename to features/xcode/fixtures/fl-project/dsyms/app2.dSYM/Contents/Resources/DWARF/app diff --git a/features/dsym/fixtures/fl-project/fastlane/Fastfile b/features/xcode/fixtures/fl-project/fastlane/Fastfile similarity index 100% rename from features/dsym/fixtures/fl-project/fastlane/Fastfile rename to features/xcode/fixtures/fl-project/fastlane/Fastfile diff --git "a/features/dsym/fixtures/fl-project/some dir/some files \316\262.app.dSYM.zip" "b/features/xcode/fixtures/fl-project/some dir/some files \316\262.app.dSYM.zip" similarity index 100% rename from "features/dsym/fixtures/fl-project/some dir/some files \316\262.app.dSYM.zip" rename to "features/xcode/fixtures/fl-project/some dir/some files \316\262.app.dSYM.zip" diff --git a/features/dsym/fixtures/macos-compressed-dsyms.zip b/features/xcode/fixtures/macos-compressed-dsyms.zip similarity index 100% rename from features/dsym/fixtures/macos-compressed-dsyms.zip rename to features/xcode/fixtures/macos-compressed-dsyms.zip diff --git a/features/dsym/fixtures/single-dsym.zip b/features/xcode/fixtures/single-dsym.zip similarity index 100% rename from features/dsym/fixtures/single-dsym.zip rename to features/xcode/fixtures/single-dsym.zip diff --git a/features/dsym/fixtures/single-dsym/app.dSYM/Contents/Resources/DWARF/app b/features/xcode/fixtures/single-dsym/app.dSYM/Contents/Resources/DWARF/app similarity index 100% rename from features/dsym/fixtures/single-dsym/app.dSYM/Contents/Resources/DWARF/app rename to features/xcode/fixtures/single-dsym/app.dSYM/Contents/Resources/DWARF/app diff --git a/features/dsym/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Info.plist b/features/xcode/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Info.plist similarity index 100% rename from features/dsym/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Info.plist rename to features/xcode/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Info.plist diff --git a/features/dsym/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Resources/DWARF/swift-package-manager b/features/xcode/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Resources/DWARF/swift-package-manager similarity index 100% rename from features/dsym/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Resources/DWARF/swift-package-manager rename to features/xcode/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Resources/DWARF/swift-package-manager diff --git a/features/dsym/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Resources/Relocations/aarch64/swift-package-manager.yml b/features/xcode/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Resources/Relocations/aarch64/swift-package-manager.yml similarity index 100% rename from features/dsym/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Resources/Relocations/aarch64/swift-package-manager.yml rename to features/xcode/fixtures/swift-package-manager/swift-package-manager.app.dSYM/Contents/Resources/Relocations/aarch64/swift-package-manager.yml diff --git a/features/dsym/swift_package_manager.feature b/features/xcode/swift_package_manager.feature similarity index 51% rename from features/dsym/swift_package_manager.feature rename to features/xcode/swift_package_manager.feature index 56c2d5ab..dcaaa7c9 100644 --- a/features/dsym/swift_package_manager.feature +++ b/features/xcode/swift_package_manager.feature @@ -1,7 +1,10 @@ #Feature: dSYM Uploads for Swift Package Manager Projects Integration Tests # # Scenario: Upload a single dSYM sourcemap using all CLI flags -# When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --overwrite --project-root=/my/project/root/ --plist=features/base-fixtures/dsym/swift-package-manager/swift-package-manager/Info.plist --scheme=swift-package-manager features/base-fixtures/dsym/swift-package-manager +# When I make the "features/base-fixtures/swift-package-manager" +# Then I wait for the build to succeed +# +# When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF --overwrite --project-root=/my/project/root/ --plist=features/base-fixtures/swift-package-manager/swift-package-manager/Info.plist --scheme=swift-package-manager features/base-fixtures/swift-package-manager # And I wait to receive 1 sourcemaps # Then the sourcemap is valid for the dSYM Build API # Then the sourcemaps Content-Type header is valid multipart form-data @@ -9,28 +12,40 @@ # And the sourcemap payload field "overwrite" equals "true" # # Scenario: Upload a single dSYM sourcemap using only api-key and a path pointing to a SPM project -# When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF features/base-fixtures/dsym/swift-package-manager +# When I make the "features/base-fixtures/swift-package-manager" +# Then I wait for the build to succeed +# +# When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF features/base-fixtures/swift-package-manager # And I wait to receive 1 sourcemaps # Then the sourcemap is valid for the dSYM Build API # Then the sourcemaps Content-Type header is valid multipart form-data # And the sourcemap payload field "apiKey" equals "1234567890ABCDEF1234567890ABCDEF" # # Scenario: Upload a single dSYM sourcemap using only api-key and a path pointing to a xcodeproj directory of a SPM project -# When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF features/base-fixtures/dsym/swift-package-manager/swift-package-manager.xcodeproj +# When I make the "features/base-fixtures/swift-package-manager" +# Then I wait for the build to succeed +# +# When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF features/base-fixtures/swift-package-manager/swift-package-manager.xcodeproj # And I wait to receive 1 sourcemaps # Then the sourcemap is valid for the dSYM Build API # Then the sourcemaps Content-Type header is valid multipart form-data # And the sourcemap payload field "apiKey" equals "1234567890ABCDEF1234567890ABCDEF" # # Scenario: Upload a single dSYM sourcemap with scheme defined in command -# When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --scheme=swift-package-manager --api-key=1234567890ABCDEF1234567890ABCDEF features/base-fixtures/dsym/swift-package-manager +# When I make the "features/base-fixtures/swift-package-manager" +# Then I wait for the build to succeed +# +# When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --scheme=swift-package-manager --api-key=1234567890ABCDEF1234567890ABCDEF features/base-fixtures/swift-package-manager # And I wait to receive 1 sourcemaps # Then the sourcemap is valid for the dSYM Build API # Then the sourcemaps Content-Type header is valid multipart form-data # And the sourcemap payload field "apiKey" equals "1234567890ABCDEF1234567890ABCDEF" # # Scenario: Upload a single dSYM sourcemap using plist to define apiKey and appVersion -# When I run bugsnag-cli with upload dsym --upload-api-root-url=http://localhost:9339 --plist=features/base-fixtures/dsym/swift-package-manager/swift-package-manager/Info.plist features/base-fixtures/dsym/swift-package-manager +# When I make the "features/base-fixtures/swift-package-manager" +# Then I wait for the build to succeed +# +# When I run bugsnag-cli with upload xcode-build --upload-api-root-url=http://localhost:9339 --plist=features/base-fixtures/swift-package-manager/swift-package-manager/Info.plist features/base-fixtures/swift-package-manager # And I wait to receive 1 sourcemaps # Then the sourcemap is valid for the dSYM Build API # Then the sourcemaps Content-Type header is valid multipart form-data diff --git a/features/xcode/xcarchive.feature b/features/xcode/xcarchive.feature new file mode 100644 index 00000000..223e481b --- /dev/null +++ b/features/xcode/xcarchive.feature @@ -0,0 +1,18 @@ +Feature: Upload Xcode Archives + Scenario: Uploading an .xcarchive containing commas and special characters + When I run bugsnag-cli with upload xcode-archive --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF "features/xcode/fixtures/bugsnag-example 14-05-2021,,, 11.27éøœåñü#.xcarchive" + And I wait to receive 2 sourcemaps + Then the sourcemap is valid for the dSYM Build API + Then the sourcemaps Content-Type header is valid multipart form-data + And the sourcemap payload field "apiKey" equals "1234567890ABCDEF1234567890ABCDEF" + + Scenario: Archive and upload an .xcarchive + When I make the "features/base-fixtures/dsym/archive" + Then I wait for the build to succeed + + + When I run bugsnag-cli with upload xcode-archive --upload-api-root-url=http://localhost:9339 --api-key=1234567890ABCDEF1234567890ABCDEF features/base-fixtures/dsym/ + And I wait to receive 1 sourcemaps + Then the sourcemap is valid for the dSYM Build API + Then the sourcemaps Content-Type header is valid multipart form-data + And the sourcemap payload field "apiKey" equals "1234567890ABCDEF1234567890ABCDEF" diff --git a/go.mod b/go.mod index ccdab633..447fbf80 100644 --- a/go.mod +++ b/go.mod @@ -7,21 +7,17 @@ require ( github.com/mattn/go-isatty v0.0.18 github.com/mitchellh/mapstructure v1.5.0 github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 golang.org/x/text v0.14.0 google.golang.org/protobuf v1.34.2 ) require ( - github.com/beevik/etree v1.2.0 // indirect - github.com/carlmjohnson/truthy v0.23.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect golang.org/x/sys v0.19.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 29694a7a..0fc9ba2c 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,17 @@ github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= +github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4= github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= -github.com/beevik/etree v1.2.0 h1:l7WETslUG/T+xOPs47dtd6jov2Ii/8/OjCldk5fYfQw= -github.com/beevik/etree v1.2.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= -github.com/carlmjohnson/truthy v0.23.1 h1:NSlOuL78OtZZZnv5/TaVBoTT2Lt2I+UJ0pVWq4xmThM= -github.com/carlmjohnson/truthy v0.23.1/go.mod h1:wBVIeaXhXEtzueUhnUaATmiXk4l23bwoD+1laRti81k= +github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -35,19 +32,12 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index 7539b83f..1412e02b 100644 --- a/main.go +++ b/main.go @@ -135,9 +135,27 @@ func main() { logger.Fatal(err.Error()) } + case "upload xcode-build", "upload xcode-build ": + + err := upload.ProcessXcodeBuild(commands, endpoint, logger) + + if err != nil { + logger.Fatal(err.Error()) + } + + case "upload xcode-archive", "upload xcode-archive ": + + err := upload.ProcessXcodeArchive(commands, endpoint, logger) + + if err != nil { + logger.Fatal(err.Error()) + } + case "upload dsym", "upload dsym ": - err := upload.ProcessDsym(commands, endpoint, logger) + logger.Warn("The `upload dsym` command is deprecated and will be removed in a future release. Please use `upload xcode-build` instead.") + + err := upload.ProcessXcodeBuild(commands, endpoint, logger) if err != nil { logger.Fatal(err.Error()) diff --git a/makefile b/makefile index c438a58b..79ba936a 100644 --- a/makefile +++ b/makefile @@ -84,6 +84,13 @@ features/base-fixtures/dsym: cd $@ && xcrun xcodebuild -allowProvisioningUpdates -scheme dSYM-Example -resolvePackageDependencies cd $@ && xcrun xcodebuild -allowProvisioningUpdates -scheme dSYM-Example -configuration Release -quiet build GCC_TREAT_WARNINGS_AS_ERRORS=YES +.PHONY: features/base-fixtures/dsym/archive +features/base-fixtures/dsym/archive: + bundle install + cd features/base-fixtures/dsym && bundle install + cd features/base-fixtures/dsym && xcrun xcodebuild -allowProvisioningUpdates -scheme dSYM-Example -resolvePackageDependencies + cd features/base-fixtures/dsym && xcrun xcodebuild -scheme dSYM-Example -configuration Release -allowProvisioningUpdates archive + .PHONY: features/base-fixtures/rn0_69 features/base-fixtures/rn0_69: features/base-fixtures/rn0_69/android features/base-fixtures/rn0_69/ios @@ -100,14 +107,14 @@ features/base-fixtures/rn0_69/android: .PHONY: features/base-fixtures/rn0_69/ios features/base-fixtures/rn0_69/ios: - cd $@/../ && npm i + cd $@/../ && npm i cd $@ && bundle install cd $@ && bundle exec pod install --repo-update cd $@ && xcodebuild -workspace rn0_69.xcworkspace -scheme rn0_69 -configuration Release -sdk iphoneos build .PHONY: features/base-fixtures/rn0_69/ios/archive features/base-fixtures/rn0_69/ios/archive: - cd features/base-fixtures/rn0_69/ && npm i + cd features/base-fixtures/rn0_69/ && npm i cd features/base-fixtures/rn0_69/ios/ && bundle install cd features/base-fixtures/rn0_69/ios/ && bundle exec pod install --repo-update cd features/base-fixtures/rn0_69/ios/ && xcrun xcodebuild -scheme rn0_69 -workspace rn0_69.xcworkspace -configuration Release -archivePath "../rn0_69.xcarchive" -allowProvisioningUpdates archive @@ -126,7 +133,7 @@ features/base-fixtures/rn0_70/ios: .PHONY: features/base-fixtures/rn0_70/ios/archive features/base-fixtures/rn0_70/ios/archive: - cd features/base-fixtures/rn0_70/ && npm i + cd features/base-fixtures/rn0_70/ && npm i cd features/base-fixtures/rn0_70/ios/ && bundle install cd features/base-fixtures/rn0_70/ios/ && bundle exec pod install --repo-update cd features/base-fixtures/rn0_70/ios/ && xcrun xcodebuild -scheme rn0_70 -workspace rn0_70.xcworkspace -configuration Release -archivePath "../rn0_70.xcarchive" -allowProvisioningUpdates archive diff --git a/pkg/ios/dsym-utils.go b/pkg/ios/dsym-utils.go index 6fd62b85..5d14c86a 100644 --- a/pkg/ios/dsym-utils.go +++ b/pkg/ios/dsym-utils.go @@ -2,16 +2,17 @@ package ios import ( "fmt" - "github.com/bugsnag/bugsnag-cli/pkg/log" "os" "os/exec" "path/filepath" "strings" + "github.com/bugsnag/bugsnag-cli/pkg/log" "github.com/bugsnag/bugsnag-cli/pkg/utils" ) -// DwarfInfo stores the UUID, architecture and name of a dwarf file +// DwarfInfo stores the UUID, architecture, name, and location of a DWARF file. +// This information is extracted from dSYM files during processing. type DwarfInfo struct { UUID string Arch string @@ -19,6 +20,19 @@ type DwarfInfo struct { Location string } +// FindDsymsInPath locates dSYM files within a specified path, processes them, +// and retrieves DWARF information for further use. +// +// Parameters: +// - path: The directory or file path to search for dSYM files. +// - ignoreEmptyDsym: If true, skips empty dSYM files without raising an error. +// - ignoreMissingDwarf: If true, skips invalid DWARF files without raising an error. +// - logger: Logger instance for informational and debug messages. +// +// Returns: +// - A slice of DwarfInfo structs containing details of found DWARF files. +// - A temporary directory if a ZIP file was extracted during processing. +// - An error if any issues occur during the process. func FindDsymsInPath(path string, ignoreEmptyDsym, ignoreMissingDwarf bool, logger log.Logger) ([]*DwarfInfo, string, error) { var tempDir string var dsymLocations []string @@ -99,47 +113,52 @@ func FindDsymsInPath(path string, ignoreEmptyDsym, ignoreMissingDwarf bool, logg return dwarfInfo, tempDir, nil } -// isDwarfDumpInstalled checks if dwarfdump is installed by checking if there is a path returned for it +// isDwarfDumpInstalled checks if the `dwarfdump` utility is available on the system. +// +// Returns: +// - `true` if the `dwarfdump` command is found in the system's executable path. +// - `false` otherwise. func isDwarfDumpInstalled() bool { return utils.LocationOf(utils.DWARFDUMP) != "" } -// getDwarfFileInfo parses dwarfdump output to easier to manage/parsable DwarfInfo structs +// getDwarfFileInfo retrieves DWARF file information from the output of the `dwarfdump` utility. +// +// Parameters: +// - path: The directory path containing the DWARF file. +// - fileName: The name of the DWARF file to be analyzed. +// +// Returns: +// - A slice of DwarfInfo structs containing extracted DWARF information. func getDwarfFileInfo(path, fileName string) []*DwarfInfo { var dwarfInfo []*DwarfInfo - - cmd := exec.Command(utils.DWARFDUMP, "-u", strings.TrimSuffix(fileName, ".zip")) + cmd := exec.Command(utils.DWARFDUMP, "-u", fileName) cmd.Dir = path output, _ := cmd.Output() - if len(output) > 0 { - outputStr := string(output) - - outputStr = strings.TrimSuffix(outputStr, "\n") - outputStr = strings.ReplaceAll(outputStr, "(", "") - outputStr = strings.ReplaceAll(outputStr, ")", "") - - outputSlice := strings.Split(outputStr, "\n") - - for _, str := range outputSlice { - if strings.Contains(str, "UUID: ") { - rawDwarfInfo := strings.Split(str, " ") - if len(rawDwarfInfo) >= 4 { - dwarf := &DwarfInfo{} - dwarf.UUID = rawDwarfInfo[1] - dwarf.Arch = rawDwarfInfo[2] - dwarf.Name = strings.Join(rawDwarfInfo[3:], " ") - dwarf.Location = path - dwarfInfo = append(dwarfInfo, dwarf) - } + for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") { + if strings.Contains(line, "UUID: ") { + parts := strings.Fields(strings.ReplaceAll(strings.ReplaceAll(line, "(", ""), ")", "")) + if len(parts) >= 4 { + dwarfInfo = append(dwarfInfo, &DwarfInfo{ + UUID: parts[1], + Arch: parts[2], + Name: strings.Join(parts[3:], " "), + Location: path, + }) } } } - return dwarfInfo } -// findDsyms walks the directory tree and returns a list of dSYM locations +// findDsyms recursively searches a directory for dSYM files. +// +// Parameters: +// - root: The root directory to search. +// +// Returns: +// - A slice of strings representing the paths to the located dSYM files. func findDsyms(root string) []string { var dsyms []string err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { diff --git a/pkg/ios/plist-reader-json.go b/pkg/ios/plist-reader-json.go index 125bf724..5b47d98d 100644 --- a/pkg/ios/plist-reader-json.go +++ b/pkg/ios/plist-reader-json.go @@ -2,6 +2,7 @@ package ios import ( "encoding/json" + "fmt" "os/exec" "github.com/pkg/errors" @@ -9,7 +10,8 @@ import ( "github.com/bugsnag/bugsnag-cli/pkg/utils" ) -// PlistData contains the relevant content of a plist file for uploading to bugsnag +// PlistData contains the relevant content of a plist file for uploading to Bugsnag. +// It extracts the app version, bundle version, and Bugsnag-specific project details. type PlistData struct { VersionName string `json:"CFBundleShortVersionString"` BundleVersion string `json:"CFBundleVersion"` @@ -20,31 +22,55 @@ type bugsnagProjectDetails struct { ApiKey string `json:"apiKey"` } -// GetPlistData returns the relevant content of a plist file as a PlistData struct +// GetPlistData parses the contents of an Info.plist file into a PlistData struct. +// +// This function uses the `plutil` command-line utility to convert the plist file +// into a JSON representation, which is then unmarshaled into a Go struct. +// +// Parameters: +// - plistFilePath: The path to the Info.plist file to be processed. +// +// Returns: +// - A pointer to a PlistData struct containing parsed plist content. +// - An error if the plist file cannot be processed, if `plutil` is unavailable, +// or if required fields are missing in the plist data. func GetPlistData(plistFilePath string) (*PlistData, error) { - var plistData *PlistData - var cmd *exec.Cmd - - if isPlutilInstalled() { - cmd = exec.Command(utils.LocationOf(utils.PLUTIL), "-convert", "json", "-o", "-", plistFilePath) - - output, err := cmd.Output() - if err != nil { - return nil, err - } - - err = json.Unmarshal(output, &plistData) - if err != nil { - return nil, err - } - } else { - return nil, errors.Errorf("Unable to locate plutil on this system.") + if plistFilePath == "" { + return nil, errors.New("plist file path is empty") } - return plistData, nil + if !isPlutilInstalled() { + return nil, errors.New("plutil is not installed or could not be located") + } + + cmd := exec.Command(utils.LocationOf(utils.PLUTIL), "-convert", "json", "-o", "-", plistFilePath) + + output, err := cmd.Output() + if err != nil { + return nil, errors.Wrapf(err, "failed to execute plutil on file: %s", plistFilePath) + } + + if len(output) == 0 { + return nil, errors.New(fmt.Sprintf("plutil returned empty output reading file: %s", plistFilePath)) + } + + var plistData PlistData + err = json.Unmarshal(output, &plistData) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("unable to parse plist data in file: %s", plistFilePath)) + } + + return &plistData, nil } -// isPlutilInstalled checks if plutil is installed by checking if there is a path returned for it +// isPlutilInstalled checks if the `plutil` utility is installed on the system. +// +// This function verifies the availability of `plutil` by checking its system path +// using a utility function. +// +// Returns: +// - `true` if `plutil` is found in the system's executable path. +// - `false` otherwise. func isPlutilInstalled() bool { return utils.LocationOf(utils.PLUTIL) != "" } diff --git a/pkg/ios/process-dsym.go b/pkg/ios/process-dsym.go new file mode 100644 index 00000000..fe6ce1d1 --- /dev/null +++ b/pkg/ios/process-dsym.go @@ -0,0 +1,78 @@ +package ios + +import ( + "fmt" + "github.com/bugsnag/bugsnag-cli/pkg/log" + "github.com/bugsnag/bugsnag-cli/pkg/options" + "github.com/bugsnag/bugsnag-cli/pkg/server" + "github.com/bugsnag/bugsnag-cli/pkg/utils" + "path/filepath" + "strings" +) + +// ProcessDsymUpload uploads dSYM files to the specified endpoint. +// It retrieves the API key from the Info.plist if not provided, builds upload options, +// and uploads each dSYM file. If the initial upload fails with a 404 error, it retries at the base endpoint. +// +// Parameters: +// - plistPath: Path to the Info.plist file. +// - endpoint: The API endpoint for uploading dSYM files. +// - projectRoot: Root directory of the project. +// - options: CLI options containing configuration like API key. +// - dwarfInfo: List of dSYM information objects to be processed. +// - logger: Logger instance for debug and error messages. +// +// Returns: +// - An error if any part of the process fails; nil otherwise. +func ProcessDsymUpload(plistPath, endpoint, projectRoot string, options options.CLI, dwarfInfo []*DwarfInfo, logger log.Logger) error { + var ( + plistData *PlistData + uploadOptions map[string]string + err error + ) + + // Retrieve API key from Info.plist if it exists and the API key is not already set. + if utils.FileExists(plistPath) && options.ApiKey == "" { + plistData, err = GetPlistData(plistPath) + if err != nil { + return fmt.Errorf("failed to read plist data: %w", err) + } + options.ApiKey = plistData.BugsnagProjectDetails.ApiKey + if options.ApiKey != "" { + logger.Debug(fmt.Sprintf("Using API key from Info.plist: %s", options.ApiKey)) + } + } + + // Process and upload each dSYM file in the provided list. + for _, dsym := range dwarfInfo { + dsymInfo := fmt.Sprintf("(UUID: %s, Name: %s, Arch: %s)", dsym.UUID, dsym.Name, dsym.Arch) + logger.Debug(fmt.Sprintf("Processing dSYM %s", dsymInfo)) + + // Build upload options for the current dSYM file. + uploadOptions, err = utils.BuildDsymUploadOptions(options.ApiKey, projectRoot) + if err != nil { + return fmt.Errorf("failed to build dSYM upload options: %w", err) + } + + // Prepare the file data for uploading. + fileFieldData := map[string]server.FileField{ + "dsym": server.LocalFile(filepath.Join(dsym.Location, dsym.Name)), + } + + // Attempt to upload the dSYM file to the endpoint. + uploadURL := endpoint + "/dsym" + err = server.ProcessFileRequest(uploadURL, uploadOptions, fileFieldData, dsym.UUID, options, logger) + if err != nil { + // Retry with the base endpoint if a 404 error occurs. + if strings.Contains(err.Error(), "404 Not Found") { + logger.Debug(fmt.Sprintf("Retrying upload for dSYM %s at base endpoint", dsymInfo)) + err = server.ProcessFileRequest(endpoint, uploadOptions, fileFieldData, dsym.UUID, options, logger) + } + if err != nil { + return fmt.Errorf("failed to upload dSYM %s: %w", dsymInfo, err) + } + } + } + + return nil +} diff --git a/pkg/ios/xcarchive.go b/pkg/ios/xcarchive.go new file mode 100644 index 00000000..76ce4a34 --- /dev/null +++ b/pkg/ios/xcarchive.go @@ -0,0 +1,99 @@ +package ios + +import ( + "bytes" + "fmt" + "github.com/bugsnag/bugsnag-cli/pkg/utils" + "os" + "os/exec" + "os/user" + "path/filepath" + "strings" + "time" +) + +// GetLatestXcodeArchiveForScheme retrieves the latest xcarchive for a given scheme. +// It determines the archive location (either custom or default) and then searches +// for the most recently modified xcarchive matching the scheme. +// +// Parameters: +// - scheme: The scheme used as a prefix to filter relevant archive files. +// +// Returns: +// - A string containing the path to the most recent xcarchive file matching the scheme. +// - An error if the location cannot be determined or if no matching archive is found. +func GetLatestXcodeArchiveForScheme(scheme string) (string, error) { + // Retrieve the xcarchive location + archivePath, err := func() (string, error) { + // Command to read the custom archive location from Xcode preferences + cmd := exec.Command("defaults", "read", "com.apple.dt.Xcode", "IDECustomDistributionArchivesLocation") + + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + + // Execute the command and capture the output + if err := cmd.Run(); err != nil { + // If the command fails, check stderr for additional details + if strings.Contains(stderr.String(), "does not exist") { + // If the key is not set, return the default location for xcarchives + usr, err := user.Current() + if err != nil { + return "", fmt.Errorf("unable to get current user: %w", err) + } + // Return default archive location under user's home directory + archivePath := filepath.Join(usr.HomeDir, "Library", "Developer", "Xcode", "Archives") + if utils.IsDir(archivePath) { + return archivePath, nil + } + } + // Return error with additional details from stderr if the command fails + return "", fmt.Errorf("error running defaults command: %w, stderr: %s", err, stderr.String()) + } + + // Trim any trailing newline or spaces from the output + customPath := strings.TrimSpace(out.String()) + if customPath == "" { + // Return error if the command succeeded but no path was returned + return "", fmt.Errorf("command succeeded but returned an empty path") + } + return customPath, nil + }() + + if err != nil { + return "", fmt.Errorf("failed to determine xcarchive location: %w", err) + } + + // Search for the latest xcarchive matching the scheme + var latestFile string + var latestModTime time.Time + + err = filepath.Walk(archivePath, func(filePath string, info os.FileInfo, err error) error { + if err != nil { + // If an error occurs while walking, return it + return err + } + + // Check if the entry is a directory, ends with ".xcarchive", and starts with the given scheme + if info.IsDir() && strings.HasSuffix(info.Name(), ".xcarchive") && strings.HasPrefix(info.Name(), scheme) { + // If this archive is newer than the previous one, update the latestFile and latestModTime + if info.ModTime().After(latestModTime) { + latestFile = filePath + latestModTime = info.ModTime() + } + } + return nil + }) + + if err != nil { + return "", fmt.Errorf("error walking the archive directory: %w", err) + } + + // If no matching xcarchive file was found, return an error + if latestFile == "" { + return "", fmt.Errorf("no xcarchive files found for scheme: %s", scheme) + } + + return latestFile, nil +} diff --git a/pkg/ios/xcodebuild-utils.go b/pkg/ios/xcodebuild-utils.go index 89c348a5..febed287 100644 --- a/pkg/ios/xcodebuild-utils.go +++ b/pkg/ios/xcodebuild-utils.go @@ -13,7 +13,7 @@ import ( "github.com/bugsnag/bugsnag-cli/pkg/utils" ) -// XcodeBuildSettings contains the relevant build settings required for uploading to bugsnag +// XcodeBuildSettings contains the relevant build settings required for uploading to BugSnag. type XcodeBuildSettings struct { ConfigurationBuildDir string `mapstructure:"CONFIGURATION_BUILD_DIR"` InfoPlistPath string `mapstructure:"INFOPLIST_PATH"` @@ -22,21 +22,36 @@ type XcodeBuildSettings struct { ProjectTempRoot string `mapstructure:"PROJECT_TEMP_ROOT"` } -// GetDefaultScheme checks if a scheme is in a given path or checks current directory if path is empty +// GetDefaultScheme determines the default Xcode scheme in a given path or the current directory if no path is provided. +// +// Parameters: +// - path (string): Path to search for schemes. +// +// Returns: +// - string: The name of the default scheme. +// - error: If no schemes or multiple schemes are found. func GetDefaultScheme(path string) (string, error) { schemes := getXcodeSchemes(path) switch len(schemes) { case 0: - return "", errors.Errorf("No schemes found in location '%s' please define which scheme to use with --scheme", path) + return "", errors.Errorf("no schemes found in location '%s'. Please specify a scheme with --scheme", path) case 1: return schemes[0], nil default: - return "", errors.Errorf("Multiple schemes found in location '%s', please define which scheme to use with --scheme", path) + return "", errors.Errorf("multiple schemes found in location '%s'. Please specify a scheme with --scheme", path) } } -// IsSchemeInPath checks if a scheme is in a given path or checks current directory if path is empty +// IsSchemeInPath verifies whether a given scheme exists in a specified path or current directory. +// +// Parameters: +// - path (string): Path to search for the scheme. +// - schemeToFind (string): Scheme name to look for. +// +// Returns: +// - bool: True if the scheme exists; false otherwise. +// - error: If the scheme cannot be located. func IsSchemeInPath(path, schemeToFind string) (bool, error) { schemes := getXcodeSchemes(path) for _, scheme := range schemes { @@ -44,28 +59,27 @@ func IsSchemeInPath(path, schemeToFind string) (bool, error) { return true, nil } } - - return false, errors.Errorf("Unable to locate scheme '%s' in location: '%s'", schemeToFind, path) + return false, errors.Errorf("unable to locate scheme '%s' in location '%s'", schemeToFind, path) } -// getXcodeSchemes parses the xcodebuild output for a given path to return a slice of schemes +// getXcodeSchemes retrieves a list of Xcode schemes by parsing the `xcodebuild` output. +// +// Parameters: +// - path (string): Path to search for schemes. +// +// Returns: +// - []string: A slice of scheme names. func getXcodeSchemes(path string) []string { var cmd *exec.Cmd if isXcodebuildInstalled() { if strings.HasSuffix(path, ".xcworkspace") { cmd = exec.Command(utils.LocationOf(utils.XCODEBUILD), "-workspace", path, "-list") - } else if strings.HasSuffix(path, ".xcodeproj") { cmd = exec.Command(utils.LocationOf(utils.XCODEBUILD), "-project", path, "-list") - } else { - cmd = exec.Command(utils.LocationOf(utils.XCODEBUILD), "-list") - - // Change the working directory of the command to path if it's a directory but not .xcodeproj or .xcworkspace - cmd.Dir = path - + cmd.Dir = path // Set working directory if path is a directory } } else { return []string{} @@ -77,9 +91,7 @@ func getXcodeSchemes(path string) []string { } schemes := strings.SplitAfterN(string(output), "Schemes:\n", 2)[1] - - // Remove excess whitespace and double newlines before splitting into a slice - schemes = strings.ReplaceAll(schemes, "\n\n", "") + schemes = strings.ReplaceAll(schemes, "\n\n", "") // Remove extra newlines schemesSlice := strings.Split(schemes, "\n") for i, scheme := range schemesSlice { @@ -89,8 +101,17 @@ func getXcodeSchemes(path string) []string { return schemesSlice } -// GetXcodeBuildSettings returns a struct of the relevant build settings for a given path and scheme -func GetXcodeBuildSettings(path, schemeName string, configuration string) (*XcodeBuildSettings, error) { +// GetXcodeBuildSettings fetches build settings for a given path, scheme, and configuration. +// +// Parameters: +// - path (string): The project or workspace path. +// - schemeName (string): The scheme to use. +// - configuration (string): The build configuration (e.g., Debug, Release). +// +// Returns: +// - *XcodeBuildSettings: A struct containing the build settings. +// - error: If the settings cannot be retrieved or decoded. +func GetXcodeBuildSettings(path, schemeName, configuration string) (*XcodeBuildSettings, error) { var buildSettings XcodeBuildSettings allBuildSettings, err := getXcodeBuildSettings(path, schemeName, configuration) if err != nil { @@ -100,46 +121,57 @@ func GetXcodeBuildSettings(path, schemeName string, configuration string) (*Xcod if err != nil { return nil, err } - return &buildSettings, nil } -// getXcodeBuildSettings parses the xcodebuild output for a given path and scheme to return a map of all build settings -func getXcodeBuildSettings(path, schemeName string, configuration string) (*map[string]*string, error) { +// getXcodeBuildSettings retrieves all build settings as a map from the `xcodebuild` output. +// +// Parameters: +// - path (string): The project or workspace path. +// - schemeName (string): The scheme to use. +// - configuration (string): The build configuration (optional). +// +// Returns: +// - *map[string]*string: A map of all build settings. +// - error: If the settings cannot be retrieved. +func getXcodeBuildSettings(path, schemeName, configuration string) (*map[string]*string, error) { var cmd *exec.Cmd - if isXcodebuildInstalled() { + if !isXcodebuildInstalled() { + return nil, fmt.Errorf("xcodebuild is not installed on this system") + } else { if !strings.HasSuffix(path, ".xcworkspace") && !strings.HasSuffix(path, ".xcodeproj") { path = FindXcodeProjOrWorkspace(path) } + var cmdArgs []string if strings.HasSuffix(path, ".xcworkspace") { - cmdArgs := []string{"-workspace", path, "-scheme", schemeName, "-showBuildSettings"} - if configuration != "" { - cmdArgs = append(cmdArgs, "-configuration", configuration) - } - cmd = exec.Command(utils.LocationOf(utils.XCODEBUILD), cmdArgs...) + cmdArgs = []string{"-workspace", path, "-scheme", schemeName, "-showBuildSettings"} } else if strings.HasSuffix(path, ".xcodeproj") { - cmdArgs := []string{"-project", path, "-scheme", schemeName, "-showBuildSettings"} - if configuration != "" { - cmdArgs = append(cmdArgs, "-configuration", configuration) - } - cmd = exec.Command(utils.LocationOf(utils.XCODEBUILD), cmdArgs...) + cmdArgs = []string{"-project", path, "-scheme", schemeName, "-showBuildSettings"} } else { - return nil, fmt.Errorf("Unable to locate xcodeproj or xcworkspace in the given path") + return nil, fmt.Errorf("unable to locate .xcodeproj or .xcworkspace in the given path") } - } else { - return nil, fmt.Errorf("Unable to locate xcodebuild on this system.") + + if configuration != "" { + cmdArgs = append(cmdArgs, "-configuration", configuration) + } + + cmd = exec.Command(utils.LocationOf(utils.XCODEBUILD), cmdArgs...) } output, err := cmd.Output() if err != nil { + if strings.Contains(err.Error(), "exit status 65") { + return nil, fmt.Errorf("scheme '%s' not found in location '%s'", schemeName, path) + } return nil, err } buildSettings := strings.SplitAfterN(string(output), "Build settings for action build and target ", 2)[1] buildSettingsSlice := strings.Split(buildSettings, "\n") buildSettingsMap := make(map[string]*string) + for _, line := range buildSettingsSlice { parts := strings.SplitN(line, "=", 2) if len(parts) == 2 { @@ -152,6 +184,13 @@ func getXcodeBuildSettings(path, schemeName string, configuration string) (*map[ return &buildSettingsMap, nil } +// IsPathAnXcodeProjectOrWorkspace checks if the given path is a .xcodeproj or .xcworkspace file. +// +// Parameters: +// - path (string): The path to check. +// +// Returns: +// - bool: True if the path is a valid Xcode project or workspace. func IsPathAnXcodeProjectOrWorkspace(path string) bool { if strings.HasSuffix(path, ".xcodeproj") || strings.HasSuffix(path, ".xcworkspace") { return true @@ -169,7 +208,14 @@ func IsPathAnXcodeProjectOrWorkspace(path string) bool { return err == nil } -// GetDefaultProjectRoot works out a value for using as project root if one isn't provided +// GetDefaultProjectRoot determines the project root directory if none is provided. +// +// Parameters: +// - path (string): The current project path. +// - projectRoot (string): The explicitly specified project root (optional). +// +// Returns: +// - string: The resolved project root directory. func GetDefaultProjectRoot(path, projectRoot string) string { if projectRoot == "" { if path == "" { @@ -178,35 +224,32 @@ func GetDefaultProjectRoot(path, projectRoot string) string { } if utils.IsDir(path) { - - // If path is pointing to a .xcodeproj or .xcworkspace directory, set the project root to one directory up if strings.HasSuffix(path, ".xcodeproj") || strings.HasSuffix(path, ".xcworkspace") { return filepath.Dir(path) - } } - - // If path is pointing to a normal directory, set that as the project root return path - - } else { - // If the project root is already set, use as-is - return projectRoot } + return projectRoot } -// isXcodebuildInstalled checks if xcodebuild is installed by checking if there is a path returned for it +// isXcodebuildInstalled checks if the `xcodebuild` command is available on the system. +// +// Returns: +// - bool: True if `xcodebuild` is installed; false otherwise. func isXcodebuildInstalled() bool { return utils.LocationOf(utils.XCODEBUILD) != "" } -// FindXcodeProjOrWorkspace finds the .xcodeproj or .xcworkspace file in a given directory -// and returns the path to it -// If neither is found, an empty string is returned -// If both are found, the .xcworkspace file is returned +// FindXcodeProjOrWorkspace searches for a .xcodeproj or .xcworkspace file in the specified directory. +// +// Parameters: +// - path (string): The directory to search. +// +// Returns: +// - string: The path to the .xcodeproj or .xcworkspace file, preferring .xcworkspace if both are found. func FindXcodeProjOrWorkspace(path string) string { - var xcodeProjPath string - var xcodeWorkspacePath string + var xcodeProjPath, xcodeWorkspacePath string files, err := os.ReadDir(path) if err != nil { @@ -223,9 +266,6 @@ func FindXcodeProjOrWorkspace(path string) string { if xcodeWorkspacePath != "" { return xcodeWorkspacePath - } else if xcodeProjPath != "" { - return xcodeProjPath } - - return "" + return xcodeProjPath } diff --git a/pkg/options/options.go b/pkg/options/options.go index caeb4b9c..62adfb29 100644 --- a/pkg/options/options.go +++ b/pkg/options/options.go @@ -61,16 +61,25 @@ type DartSymbol struct { VersionCode string `help:"The version code of this build of the application (Android only)" xor:"app-version-code,version-code"` } -type Dsym struct { - Path utils.Paths `arg:"" name:"path" help:"The path to the directory or file to upload" type:"path" default:"."` - IgnoreEmptyDsym bool `help:"Throw warnings instead of errors when a dSYM file is found, rather than the expected dSYM directory"` - IgnoreMissingDwarf bool `help:"Throw warnings instead of errors when a dSYM with missing DWARF data is found"` - Plist utils.Path `help:"The path to a .plist file from which to obtain build information" type:"path"` - Scheme string `help:"The name of the Xcode options.Scheme used to build the application"` - VersionName string `help:"The version of the application"` - XcodeProject utils.Path `help:"The path to an Xcode project, workspace or containing directory from which to obtain build information" type:"path"` - ProjectRoot string `help:"The path to strip from the beginning of source file names referenced in stacktraces on the BugSnag dashboard" type:"path"` - Configuration string `help:"The configuration used to build the application"` +type XcodeBuild struct { + Path utils.Paths `arg:"" name:"path" help:"The path to the directory or file to upload" type:"path" default:"."` + Plist utils.Path `help:"The path to a .plist file from which to obtain build information" type:"path"` + VersionName string `help:"The version of the application"` + XcodeProject utils.Path `help:"The path to an Xcode project, workspace or containing directory from which to obtain build information" type:"path"` + Configuration string `help:"The configuration used to build the application"` + Shared XcodeShared `embed:""` +} + +type XcodeArchive struct { + Path utils.Paths `arg:"" name:"path" help:"The path to the directory or file to upload" type:"path" default:"."` + Shared XcodeShared `embed:""` +} + +type XcodeShared struct { + IgnoreEmptyDsym bool `help:"Throw warnings instead of errors when a dSYM file is found, rather than the expected dSYM directory"` + IgnoreMissingDwarf bool `help:"Throw warnings instead of errors when a dSYM with missing DWARF data is found"` + Scheme string `help:"The name of the Xcode options.Scheme used to build the application"` + ProjectRoot string `help:"The path to strip from the beginning of source file names referenced in stacktraces on the BugSnag dashboard" type:"path"` } type Js struct { @@ -159,7 +168,9 @@ type CLI struct { AndroidNdk AndroidNdkMapping `cmd:"" help:"Process and upload NDK symbol files for Android"` AndroidProguard AndroidProguardMapping `cmd:"" help:"Process and upload Proguard/R8 mapping files for Android"` DartSymbol DartSymbol `cmd:"" help:"Process and upload symbol files for Flutter" name:"dart"` - Dsym Dsym `cmd:"" help:"Upload dSYMs for iOS"` + XcodeBuild XcodeBuild `cmd:"" help:"Upload dSYMs for iOS from a build"` + Dsym XcodeBuild `cmd:"" help:"(deprecated) Upload dSYMs for iOS"` + XcodeArchive XcodeArchive `cmd:"" help:"Upload dSYMs for iOS from a Xcarchive"` Js Js `cmd:"" help:"Upload source maps for JavaScript"` ReactNative ReactNative `cmd:"" help:"Upload source maps for React Native"` ReactNativeAndroid ReactNativeAndroid `cmd:"" help:"Upload source maps for React Native Android"` diff --git a/pkg/server/request.go b/pkg/server/request.go index 601c0d33..33f98481 100644 --- a/pkg/server/request.go +++ b/pkg/server/request.go @@ -138,7 +138,7 @@ func ProcessFileRequest(endpoint string, uploadOptions map[string]string, fileFi } } else { logger.Info(fmt.Sprintf("(dryrun) Skipping upload of %s to %s", filepath.Base(fileName), endpoint)) - logger.Info("(dryrun) Upload payload:") + logger.Debug("(dryrun) Upload payload:") prettyUploadOptions, _ := utils.PrettyPrintMap(uploadOptions) logger.Debug(prettyUploadOptions) } diff --git a/pkg/upload/dsym.go b/pkg/upload/dsym.go deleted file mode 100644 index 9e240c5d..00000000 --- a/pkg/upload/dsym.go +++ /dev/null @@ -1,162 +0,0 @@ -package upload - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/bugsnag/bugsnag-cli/pkg/ios" - "github.com/bugsnag/bugsnag-cli/pkg/log" - "github.com/bugsnag/bugsnag-cli/pkg/options" - "github.com/bugsnag/bugsnag-cli/pkg/server" - "github.com/bugsnag/bugsnag-cli/pkg/utils" -) - -func ProcessDsym(options options.CLI, endpoint string, logger log.Logger) error { - dsymOptions := options.Upload.Dsym - var buildSettings *ios.XcodeBuildSettings - var plistData *ios.PlistData - var uploadOptions map[string]string - - var dwarfInfo []*ios.DwarfInfo - var tempDirs []string - var dsymPath string - var err error - var tempDir string - xcodeProjPath := string(dsymOptions.XcodeProject) - plistPath := string(dsymOptions.Plist) - - // Performs an automatic cleanup of temporary directories at the end - defer func() { - for _, tempDir := range tempDirs { - _ = os.RemoveAll(tempDir) - } - }() - - for _, path := range dsymOptions.Path { - if ios.IsPathAnXcodeProjectOrWorkspace(path) { - if xcodeProjPath == "" { - xcodeProjPath = path - } - } else { - dsymPath = path - } - - if xcodeProjPath != "" { - if dsymOptions.ProjectRoot == "" { - dsymOptions.ProjectRoot = ios.GetDefaultProjectRoot(xcodeProjPath, dsymOptions.ProjectRoot) - logger.Info(fmt.Sprintf("Setting `--project-root` from Xcode project settings: %s", dsymOptions.ProjectRoot)) - } - - // Get build settings and dsymPath - // If options.Scheme is set explicitly, check if it exists - if dsymOptions.Scheme != "" { - _, err := ios.IsSchemeInPath(xcodeProjPath, dsymOptions.Scheme) - if err != nil { - logger.Warn(err.Error()) - } - } else { - // Otherwise, try to find it - dsymOptions.Scheme, err = ios.GetDefaultScheme(xcodeProjPath) - if err != nil { - logger.Warn(err.Error()) - } - } - - if dsymOptions.Scheme != "" { - buildSettings, err = ios.GetXcodeBuildSettings(xcodeProjPath, dsymOptions.Scheme, options.Upload.Dsym.Configuration) - if err != nil { - logger.Warn(err.Error()) - } - } - - if buildSettings != nil && dsymPath == "" { - // Build the dsymPath from build settings - // Which is built up to look like: /Users/Path/To/Config/Build/Dir/MyApp.app.dSYM - possibleDsymPath := filepath.Join(buildSettings.ConfigurationBuildDir, buildSettings.DsymName) - - // Check if dsymPath exists before proceeding - _, err := os.Stat(possibleDsymPath) - if err == nil { - dsymPath = possibleDsymPath - logger.Debug(fmt.Sprintf("Using dSYM path: %s", dsymPath)) - } - } - } - - if dsymOptions.ProjectRoot == "" { - dsymOptions.ProjectRoot, _ = os.Getwd() - logger.Info(fmt.Sprintf("Setting `--project-root` to current working directory: %s", dsymOptions.ProjectRoot)) - } - - if dsymPath == "" { - return fmt.Errorf("No dSYM locations detected. Please provide a valid dSYM path or an Xcode project/workspace path") - } - - dwarfInfo, tempDir, err = ios.FindDsymsInPath(dsymPath, dsymOptions.IgnoreEmptyDsym, dsymOptions.IgnoreMissingDwarf, logger) - tempDirs = append(tempDirs, tempDir) - if err != nil { - return err - } else if len(dwarfInfo) == 0 { - return fmt.Errorf("No dSYM files found in: %s", dsymPath) - } - - // If the Info.plist path is not defined, we need to build the path to Info.plist from build settings values - if plistPath == "" && options.ApiKey == "" { - if buildSettings != nil { - plistPathExpected := filepath.Join(buildSettings.ConfigurationBuildDir, buildSettings.InfoPlistPath) - if utils.FileExists(plistPathExpected) { - plistPath = plistPathExpected - logger.Debug(fmt.Sprintf("Found Info.plist at expected location: %s", plistPath)) - } else { - logger.Debug(fmt.Sprintf("No Info.plist found at expected location: %s", plistPathExpected)) - } - } - } - - // If the Info.plist path is defined and we still don't know the apiKey try to extract them from it - if plistPath != "" && options.ApiKey == "" { - // Read data from the plist - plistData, err = ios.GetPlistData(plistPath) - if err != nil { - return err - } - - if options.ApiKey == "" { - options.ApiKey = plistData.BugsnagProjectDetails.ApiKey - if options.ApiKey != "" { - logger.Debug(fmt.Sprintf("Using API key from Info.plist: %s", options.ApiKey)) - } - } - } - - for _, dsym := range dwarfInfo { - dsymInfo := fmt.Sprintf("(UUID: %s, Name: %s, Arch: %s)", dsym.UUID, dsym.Name, dsym.Arch) - logger.Debug(fmt.Sprintf("Processing dSYM %s", dsymInfo)) - - uploadOptions, err = utils.BuildDsymUploadOptions(options.ApiKey, dsymOptions.ProjectRoot) - if err != nil { - return err - } - - fileFieldData := make(map[string]server.FileField) - fileFieldData["dsym"] = server.LocalFile(filepath.Join(dsym.Location, dsym.Name)) - - err = server.ProcessFileRequest(endpoint+"/dsym", uploadOptions, fileFieldData, dsym.UUID, options, logger) - - if err != nil { - if strings.Contains(err.Error(), "404 Not Found") { - err = server.ProcessFileRequest(endpoint, uploadOptions, fileFieldData, dsym.UUID, options, logger) - } - } - - if err != nil { - - return err - } - } - } - - return nil -} diff --git a/pkg/upload/react-native-ios.go b/pkg/upload/react-native-ios.go index fc477b42..97ac7b97 100644 --- a/pkg/upload/react-native-ios.go +++ b/pkg/upload/react-native-ios.go @@ -15,6 +15,7 @@ import ( func ProcessReactNativeIos(options options.CLI, endpoint string, logger log.Logger) error { iosOptions := options.Upload.ReactNativeIos var rootDirPath string + var plistData *ios.PlistData var buildSettings *ios.XcodeBuildSettings var err error @@ -159,7 +160,7 @@ func ProcessReactNativeIos(options options.CLI, endpoint string, logger log.Logg if iosOptions.Ios.Plist != "" && (options.ApiKey == "" || iosOptions.ReactNative.VersionName == "" || iosOptions.Ios.BundleVersion == "") { // Read data from the plist - plistData, err := ios.GetPlistData(iosOptions.Ios.Plist) + plistData, err = ios.GetPlistData(iosOptions.Ios.Plist) if err != nil { return err } diff --git a/pkg/upload/react-native.go b/pkg/upload/react-native.go index 2d6d804f..43fa1d47 100644 --- a/pkg/upload/react-native.go +++ b/pkg/upload/react-native.go @@ -1,6 +1,7 @@ package upload import ( + "fmt" "path/filepath" "github.com/bugsnag/bugsnag-cli/pkg/log" @@ -8,17 +9,16 @@ import ( "github.com/bugsnag/bugsnag-cli/pkg/utils" ) +// ProcessReactNative handles the upload process for React Native projects. func ProcessReactNative(globalOptions options.CLI, endpoint string, logger log.Logger) error { reactNativeOptions := globalOptions.Upload.ReactNative - // The commands must be run from either the android/ or ios/ subdirectory. - androidPath := []string{} - iosPath := []string{} - for _, basePath := range reactNativeOptions.Path { - androidPath = append(androidPath, filepath.Join(basePath, "android")) - iosPath = append(iosPath, filepath.Join(basePath, "ios")) - } + // Construct Android and iOS paths + androidPath, iosPath := generatePaths(reactNativeOptions.Path, "android", "ios") + + logger.Info("Starting upload of React Native assets") + // Process React Native Android logger.Info("Uploading JavaScript source maps for Android") globalOptions.Upload.ReactNativeAndroid = options.ReactNativeAndroid{ Path: androidPath, @@ -27,9 +27,10 @@ func ProcessReactNative(globalOptions options.CLI, endpoint string, logger log.L Android: reactNativeOptions.AndroidSpecific, } if err := ProcessReactNativeAndroid(globalOptions, endpoint, logger); err != nil { - return err + return fmt.Errorf("failed to upload JavaScript source maps for Android: %w", err) } + // Process React Native iOS logger.Info("Uploading JavaScript source maps for iOS") globalOptions.Upload.ReactNativeIos = options.ReactNativeIos{ Path: iosPath, @@ -38,11 +39,11 @@ func ProcessReactNative(globalOptions options.CLI, endpoint string, logger log.L Ios: reactNativeOptions.IosSpecific, } if err := ProcessReactNativeIos(globalOptions, endpoint, logger); err != nil { - return err + return fmt.Errorf("failed to upload JavaScript source maps for iOS: %w", err) } + // Process Android Proguard mappings logger.Info("Uploading Android Proguard mappings") - // Missing: ApplicationId BuildUuid NoBuildUuid DexFiles globalOptions.Upload.AndroidProguard = options.AndroidProguardMapping{ Path: androidPath, VersionName: reactNativeOptions.Shared.VersionName, @@ -51,25 +52,27 @@ func ProcessReactNative(globalOptions options.CLI, endpoint string, logger log.L VersionCode: reactNativeOptions.AndroidSpecific.VersionCode, } if err := ProcessAndroidProguard(globalOptions, endpoint, logger); err != nil { - return err + return fmt.Errorf("failed to upload Android Proguard mappings: %w", err) } + // Process iOS dSYMs logger.Info("Uploading iOS dSYMs") - // Missing: IgnoreEmptyDsym IgnoreMissingDwarf - globalOptions.Upload.Dsym = options.Dsym{ - Path: iosPath, - VersionName: reactNativeOptions.Shared.VersionName, - ProjectRoot: reactNativeOptions.ProjectRoot, + globalOptions.Upload.XcodeBuild = options.XcodeBuild{ + Path: iosPath, + VersionName: reactNativeOptions.Shared.VersionName, + Shared: options.XcodeShared{ + ProjectRoot: reactNativeOptions.ProjectRoot, + Scheme: reactNativeOptions.IosSpecific.Scheme, + }, Plist: utils.Path(reactNativeOptions.IosSpecific.Plist), - Scheme: reactNativeOptions.IosSpecific.Scheme, XcodeProject: utils.Path(reactNativeOptions.IosSpecific.XcodeProject), } - if err := ProcessDsym(globalOptions, endpoint, logger); err != nil { - return err + if err := ProcessXcodeBuild(globalOptions, endpoint, logger); err != nil { + return fmt.Errorf("failed to upload iOS dSYMs: %w", err) } + // Process Android NDK symbols logger.Info("Uploading Android NDK symbols") - // Missing: ApplicationId AndroidNdkRoot globalOptions.Upload.AndroidNdk = options.AndroidNdkMapping{ Path: androidPath, VersionName: reactNativeOptions.Shared.VersionName, @@ -79,8 +82,19 @@ func ProcessReactNative(globalOptions options.CLI, endpoint string, logger log.L VersionCode: reactNativeOptions.AndroidSpecific.VersionCode, } if err := ProcessAndroidNDK(globalOptions, endpoint, logger); err != nil { - return err + return fmt.Errorf("failed to upload Android NDK symbols: %w", err) } + logger.Info("Successfully uploaded all React Native assets") return nil } + +// generatePaths constructs platform-specific paths based on the base paths provided. +func generatePaths(basePaths []string, androidSubPath, iosSubPath string) ([]string, []string) { + var androidPaths, iosPaths []string + for _, basePath := range basePaths { + androidPaths = append(androidPaths, filepath.Join(basePath, androidSubPath)) + iosPaths = append(iosPaths, filepath.Join(basePath, iosSubPath)) + } + return androidPaths, iosPaths +} diff --git a/pkg/upload/xcode-archive.go b/pkg/upload/xcode-archive.go new file mode 100644 index 00000000..bb4c63ed --- /dev/null +++ b/pkg/upload/xcode-archive.go @@ -0,0 +1,97 @@ +package upload + +import ( + "fmt" + "github.com/bugsnag/bugsnag-cli/pkg/ios" + "github.com/bugsnag/bugsnag-cli/pkg/log" + "github.com/bugsnag/bugsnag-cli/pkg/options" + "github.com/bugsnag/bugsnag-cli/pkg/utils" + "os" + "path/filepath" +) + +// ProcessXcodeArchive processes an xcarchive, locating its dSYM files and uploading them +// to a Bugsnag server. +// +// Parameters: +// - options: CLI options provided by the user, including xcarchive settings. +// - endpoint: The server endpoint for uploading dSYM files. +// - logger: Logger instance for logging messages during processing. +// +// Returns: +// - An error if any part of the process fails, otherwise nil. +func ProcessXcodeArchive(options options.CLI, endpoint string, logger log.Logger) error { + xcarchiveOptions := options.Upload.XcodeArchive + var ( + xcarchivePath string + plistPath string + dwarfInfo []*ios.DwarfInfo + tempDirs []string + tempDir string + err error + ) + + // Ensure temporary directories are cleaned up after execution + defer func() { + for _, tempDir := range tempDirs { + _ = os.RemoveAll(tempDir) + } + }() + + // Search for an xcarchive in the specified paths + for _, path := range xcarchiveOptions.Path { + if filepath.Ext(path) == ".xcarchive" { + xcarchivePath = path + } else if utils.IsDir(path) { + logger.Info(fmt.Sprintf("Searching for xcarchives in %s", path)) + if ios.IsPathAnXcodeProjectOrWorkspace(path) { + xcarchiveOptions.Shared.ProjectRoot = ios.GetDefaultProjectRoot(path, xcarchiveOptions.Shared.ProjectRoot) + logger.Info(fmt.Sprintf("Setting `--project-root` from Xcode project settings: %s", xcarchiveOptions.Shared.ProjectRoot)) + + if xcarchiveOptions.Shared.Scheme == "" { + xcarchiveOptions.Shared.Scheme, err = ios.GetDefaultScheme(path) + if err != nil { + return fmt.Errorf("Error determining default scheme: %w", err) + } + } + + xcarchivePath, err = ios.GetLatestXcodeArchiveForScheme(xcarchiveOptions.Shared.Scheme) + if err != nil { + return fmt.Errorf("Error locating latest xcarchive: %w", err) + } + } else { + return fmt.Errorf("No xcarchive found in %s", path) + } + } + + if xcarchivePath == "" { + return fmt.Errorf("No xcarchive found in specified paths") + } + + logger.Info(fmt.Sprintf("Found xcarchive at %s", xcarchivePath)) + + // Locate and process dSYM files in the xcarchive + dwarfInfo, tempDir, err = ios.FindDsymsInPath( + xcarchivePath, + xcarchiveOptions.Shared.IgnoreEmptyDsym, + xcarchiveOptions.Shared.IgnoreMissingDwarf, + logger, + ) + tempDirs = append(tempDirs, tempDir) + if err != nil { + return fmt.Errorf("Error locating dSYM files: %w", err) + } + if len(dwarfInfo) == 0 { + return fmt.Errorf("No dSYM files found in: %s", xcarchivePath) + } + logger.Info(fmt.Sprintf("Found %d dSYM files in %s", len(dwarfInfo), xcarchivePath)) + + // Extract API key from Info.plist if available + plistPath = filepath.Join(xcarchivePath, "Info.plist") + err = ios.ProcessDsymUpload(plistPath, endpoint, xcarchiveOptions.Shared.ProjectRoot, options, dwarfInfo, logger) + if err != nil { + return fmt.Errorf("Error uploading dSYM files: %w", err) + } + } + return nil +} diff --git a/pkg/upload/xcode-build.go b/pkg/upload/xcode-build.go new file mode 100644 index 00000000..50355316 --- /dev/null +++ b/pkg/upload/xcode-build.go @@ -0,0 +1,130 @@ +package upload + +import ( + "fmt" + "github.com/bugsnag/bugsnag-cli/pkg/ios" + "github.com/bugsnag/bugsnag-cli/pkg/log" + "github.com/bugsnag/bugsnag-cli/pkg/options" + "os" + "path/filepath" +) + +// ProcessXcodeBuild processes an Xcode build, locates necessary dSYM files, and uploads them +// to a Bugsnag server using the provided Xcode project or workspace configuration. +// +// Parameters: +// - options: CLI options provided by the user, including Xcode build settings. +// - endpoint: The server endpoint for uploading dSYM files. +// - logger: Logger instance for logging messages during processing. +// +// Returns: +// - An error if any part of the process fails, otherwise nil. +func ProcessXcodeBuild(options options.CLI, endpoint string, logger log.Logger) error { + dsymOptions := options.Upload.XcodeBuild + var ( + buildSettings *ios.XcodeBuildSettings + dwarfInfo []*ios.DwarfInfo + tempDirs []string + dsymPath string + tempDir string + err error + ) + xcodeProjPath := string(dsymOptions.XcodeProject) + plistPath := string(dsymOptions.Plist) + + // Cleanup temporary directories on exit + defer func() { + for _, tempDir := range tempDirs { + _ = os.RemoveAll(tempDir) + } + }() + + // Process paths provided in the CLI options + for _, path := range dsymOptions.Path { + if filepath.Ext(path) == ".xcarchive" { + logger.Warn(fmt.Sprintf("The specified path %s is an xcarchive. Please use the `xcode-archive` command instead as this functionality will be deprecated in future releases.", path)) + } + + if ios.IsPathAnXcodeProjectOrWorkspace(path) { + // Use the first valid Xcode project/workspace path + if xcodeProjPath == "" { + xcodeProjPath = path + } + } else { + // Assume the path is a dSYM file location + dsymPath = path + } + + if xcodeProjPath != "" { + // Determine project root if not provided + if dsymOptions.Shared.ProjectRoot == "" { + dsymOptions.Shared.ProjectRoot = ios.GetDefaultProjectRoot(xcodeProjPath, dsymOptions.Shared.ProjectRoot) + logger.Info(fmt.Sprintf("Setting `--project-root` from Xcode project settings: %s", dsymOptions.Shared.ProjectRoot)) + } + + // Determine or validate the scheme + if dsymOptions.Shared.Scheme == "" { + dsymOptions.Shared.Scheme, err = ios.GetDefaultScheme(xcodeProjPath) + if err != nil { + logger.Warn(fmt.Sprintf("Error determining default scheme: %s", err)) + } + } else { + _, err = ios.IsSchemeInPath(xcodeProjPath, dsymOptions.Shared.Scheme) + if err != nil { + logger.Warn(fmt.Sprintf("Scheme validation error: %s", err)) + } + } + + // Retrieve build settings for the scheme and configuration + if dsymOptions.Shared.Scheme != "" { + buildSettings, err = ios.GetXcodeBuildSettings(xcodeProjPath, dsymOptions.Shared.Scheme, dsymOptions.Configuration) + if err != nil { + logger.Warn(fmt.Sprintf("Error retrieving build settings: %s", err)) + } + } + + // Construct the dSYM path if not already specified + if buildSettings != nil && dsymPath == "" { + possibleDsymPath := filepath.Join(buildSettings.ConfigurationBuildDir, buildSettings.DsymName) + if _, err = os.Stat(possibleDsymPath); err == nil { + dsymPath = possibleDsymPath + logger.Debug(fmt.Sprintf("Using dSYM path: %s", dsymPath)) + } + } + } + + // Default project root to current directory if not set + if dsymOptions.Shared.ProjectRoot == "" { + dsymOptions.Shared.ProjectRoot, _ = os.Getwd() + logger.Info(fmt.Sprintf("Setting `--project-root` to current working directory: %s", dsymOptions.Shared.ProjectRoot)) + } + + // Validate dSYM path + if dsymPath == "" { + return fmt.Errorf("No dSYM locations detected. Provide a valid dSYM path or Xcode project/workspace path") + } + + // Locate and process dSYM files + dwarfInfo, tempDir, err = ios.FindDsymsInPath(dsymPath, dsymOptions.Shared.IgnoreEmptyDsym, dsymOptions.Shared.IgnoreMissingDwarf, logger) + tempDirs = append(tempDirs, tempDir) + if err != nil { + return fmt.Errorf("Error locating dSYM files: %w", err) + } + if len(dwarfInfo) == 0 { + return fmt.Errorf("No dSYM files found in: %s", dsymPath) + } + + // Locate Info.plist if not already specified + if plistPath == "" && options.ApiKey == "" && buildSettings != nil { + plistPath = filepath.Join(buildSettings.ConfigurationBuildDir, buildSettings.InfoPlistPath) + } + + // Upload dSYM files + err = ios.ProcessDsymUpload(plistPath, endpoint, dsymOptions.Shared.ProjectRoot, options, dwarfInfo, logger) + if err != nil { + return fmt.Errorf("Error uploading dSYM files: %w", err) + } + } + + return nil +} diff --git a/pkg/utils/files.go b/pkg/utils/files.go index fa067a42..46829772 100644 --- a/pkg/utils/files.go +++ b/pkg/utils/files.go @@ -15,10 +15,20 @@ const ( DWARFDUMP = "dwarfdump" ) -// FilePathWalkDir - finds files within a given directory +// FilePathWalkDir recursively finds all files within a given directory. +// +// Parameters: +// - root (string): The root directory to start searching from. +// +// Returns: +// - []string: A slice of file paths found within the directory. +// - error: Any error encountered during the walk process. func FilePathWalkDir(root string) ([]string, error) { var files []string err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } if !info.IsDir() { files = append(files, path) } @@ -27,14 +37,26 @@ func FilePathWalkDir(root string) ([]string, error) { return files, err } -// IsDir - Checks if a provided path is a directory or not +// IsDir checks if the provided path is a directory. +// +// Parameters: +// - path (string): The path to check. +// +// Returns: +// - bool: True if the path is a directory; false otherwise. func IsDir(path string) bool { pathInfo, err := os.Stat(path) - return err == nil && pathInfo.IsDir() } -// BuildFileList - Builds a list of files from a given path(s) +// BuildFileList compiles a list of files from the provided paths. +// +// Parameters: +// - paths ([]string): A slice of paths to process. +// +// Returns: +// - []string: A slice containing file paths from directories and standalone files. +// - error: Any error encountered during processing. func BuildFileList(paths []string) ([]string, error) { var fileList []string @@ -53,39 +75,58 @@ func BuildFileList(paths []string) ([]string, error) { return fileList, nil } -// BuildDirectoryList - Builds a list of directories from a given path(s) +// BuildDirectoryList compiles a list of directories from the provided paths. +// +// Parameters: +// - paths ([]string): A slice of paths to process. +// +// Returns: +// - []string: A slice containing the base names of subdirectories. +// - error: Any error encountered during processing. func BuildDirectoryList(paths []string) ([]string, error) { var directoryList []string for _, directory := range paths { if IsDir(directory) { err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { - if err == nil && info.IsDir() { - if directory != path { - directoryList = append(directoryList, filepath.Base(path)) - } + if err != nil { + return err + } + if info.IsDir() && directory != path { + directoryList = append(directoryList, filepath.Base(path)) } return nil }) - if err != nil { - return directoryList, err + return nil, err } } } return directoryList, nil } -// FileExists - Checks if a given file exists on the system +// FileExists checks if a given file exists. +// +// Parameters: +// - path (string): The file path to check. +// +// Returns: +// - bool: True if the file exists; false otherwise. func FileExists(path string) bool { - if _, err := os.Stat(path); err != nil { - return false - } - return true + _, err := os.Stat(path) + return err == nil } -// FindLatestFileWithSuffix - Finds the latest file with a given suffix -func FindLatestFileWithSuffix(directory string, targetSuffix string) (string, error) { +// FindLatestFileWithSuffix searches for the most recently modified file with a given suffix. +// +// Parameters: +// - directory (string): The directory to search in. +// - targetSuffix (string): The suffix to match. +// +// Returns: +// - string: The path to the newest file matching the suffix. +// - error: Any error encountered during the search. +func FindLatestFileWithSuffix(directory, targetSuffix string) (string, error) { var newestFile string var newestModTime time.Time @@ -93,46 +134,54 @@ func FindLatestFileWithSuffix(directory string, targetSuffix string) (string, er if err != nil { return err } - - if !info.IsDir() && strings.HasSuffix(path, targetSuffix) { - // Check to see if the file that we have found is newer than the previous file - if info.ModTime().After(newestModTime) { - newestModTime = info.ModTime() - newestFile = path - } + if !info.IsDir() && strings.HasSuffix(path, targetSuffix) && info.ModTime().After(newestModTime) { + newestModTime = info.ModTime() + newestFile = path } - return nil }) if err != nil { return "", err } - if newestFile == "" { - return "", fmt.Errorf("Unable to find %s files in %s", targetSuffix, directory) + return "", fmt.Errorf("unable to find files with suffix '%s' in '%s'", targetSuffix, directory) } - return newestFile, err + return newestFile, nil } -func ExtractFile(file string, slug string) (string, error) { +// ExtractFile extracts the contents of a file into a temporary directory. +// +// Parameters: +// - file (string): The file to extract. +// - slug (string): A unique identifier for the temporary directory. +// +// Returns: +// - string: The path to the temporary directory containing the extracted files. +// - error: Any error encountered during extraction. +func ExtractFile(file, slug string) (string, error) { tempDir, err := os.MkdirTemp("", fmt.Sprintf("bugsnag-cli-%s-unpacking-*", slug)) - if err != nil { - return "", fmt.Errorf("error creating temporary working directory %s", err.Error()) + return "", fmt.Errorf("error creating temporary working directory: %s", err.Error()) } - err = Unzip(file, tempDir) - - if err != nil { + if err := Unzip(file, tempDir); err != nil { return "", err } return tempDir, nil } -// FindFolderWithSuffix finds a folder with a given suffix +// FindFolderWithSuffix searches for the first folder with a specified suffix. +// +// Parameters: +// - rootPath (string): The root directory to search in. +// - targetSuffix (string): The suffix to match. +// +// Returns: +// - string: The path to the matching folder. +// - error: Any error encountered during the search. func FindFolderWithSuffix(rootPath, targetSuffix string) (string, error) { var matchingFolder string @@ -140,23 +189,23 @@ func FindFolderWithSuffix(rootPath, targetSuffix string) (string, error) { if err != nil { return err } - if info.IsDir() && strings.HasSuffix(info.Name(), targetSuffix) { matchingFolder = path return filepath.SkipDir } - return nil }) - if err != nil { - return "", err - } - - return matchingFolder, nil + return matchingFolder, err } -// LocationOf returns the path of the executable file associated with the given command. +// LocationOf determines the path of the executable associated with a given command. +// +// Parameters: +// - something (string): The command to locate. +// +// Returns: +// - string: The path to the executable or an empty string if not found. func LocationOf(something string) string { cmd := exec.Command("which", something) location, _ := cmd.Output() From 02e6b901af7dc77fe9a549c3964fb437a56054dc Mon Sep 17 00:00:00 2001 From: Josh Edney Date: Mon, 6 Jan 2025 13:38:36 +0000 Subject: [PATCH 2/6] bump version --- install.sh | 2 +- main.go | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 0784f8b5..fa999013 100755 --- a/install.sh +++ b/install.sh @@ -91,7 +91,7 @@ display_help() { EOS } -VERSION="2.7.0" +VERSION="2.8.0" while [[ "$#" -gt 0 ]]; do case "$1" in diff --git a/main.go b/main.go index 1412e02b..784b0d49 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( "github.com/bugsnag/bugsnag-cli/pkg/utils" ) -var package_version = "2.7.0" +var package_version = "2.8.0" func main() { commands := options.CLI{} diff --git a/package.json b/package.json index 57c04dd6..3e1e0019 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/cli", - "version": "2.7.0", + "version": "2.8.0", "description": "BugSnag CLI", "main": "install.js", "bin": { From 51b5a8faf9f7a893e4030e4f7f0a8117b375e9e1 Mon Sep 17 00:00:00 2001 From: Josh Edney Date: Mon, 20 Jan 2025 09:32:13 +0000 Subject: [PATCH 3/6] add some extra debug logs to android aab upload process --- pkg/upload/android-aab.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/upload/android-aab.go b/pkg/upload/android-aab.go index 34eec966..5e39309e 100644 --- a/pkg/upload/android-aab.go +++ b/pkg/upload/android-aab.go @@ -1,6 +1,7 @@ package upload import ( + "fmt" "os" "path/filepath" @@ -38,6 +39,7 @@ func ProcessAndroidAab(globalOptions options.CLI, endpoint string, logger log.Lo } if aabFile != "" && aabDir == "" { + logger.Debug(fmt.Sprintf("Extracting AAB file: %s", aabFile)) aabDir, err = utils.ExtractFile(aabFile, "aab") defer os.RemoveAll(aabDir) @@ -57,6 +59,7 @@ func ProcessAndroidAab(globalOptions options.CLI, endpoint string, logger log.Lo soFilePath := filepath.Join(aabDir, "BUNDLE-METADATA", "com.android.tools.build.debugsymbols") if utils.FileExists(soFilePath) { + logger.Debug(fmt.Sprintf("Found NDK (.so) files at: %s", soFilePath)) soFileList, err := utils.BuildFileList([]string{soFilePath}) if err != nil { @@ -87,6 +90,7 @@ func ProcessAndroidAab(globalOptions options.CLI, endpoint string, logger log.Lo mappingFilePath := filepath.Join(aabDir, "BUNDLE-METADATA", "com.android.tools.build.obfuscation", "proguard.map") if utils.FileExists(mappingFilePath) { + logger.Debug(fmt.Sprintf("Found Proguard (mapping.txt) file at: %s", mappingFilePath)) globalOptions.Upload.AndroidProguard = options.AndroidProguardMapping{ ApplicationId: manifestData["applicationId"], BuildUuid: manifestData["buildUuid"], From 589bb3ad1d3eb68902e40197941cdf78cca825b5 Mon Sep 17 00:00:00 2001 From: Josh Edney Date: Mon, 20 Jan 2025 09:35:38 +0000 Subject: [PATCH 4/6] bump changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f63f5b02..344c1dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Enhancements - Add a wrapper for the `npm` package to interact with the BugSnag CLI [161](https://github.com/bugsnag/bugsnag-cli/pull/161) - Add the support for .so.* files when processing ndk symbol files [163](https://github.com/bugsnag/bugsnag-cli/pull/163) +- Add additional logging to the Android AAB upload command [165](https://github.com/bugsnag/bugsnag-cli/pull/165) ## 2.8.0 (2025-01-06) From 69a7fb1f5b9b0dbb09ef7a598e1351bd68cf1d4c Mon Sep 17 00:00:00 2001 From: Josh Edney Date: Mon, 20 Jan 2025 09:41:35 +0000 Subject: [PATCH 5/6] bump version --- CHANGELOG.md | 2 +- install.sh | 2 +- main.go | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f63f5b02..8f4c697a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Changelog -## TBD +## 2.9.0 (2025-01-20) ### Enhancements - Add a wrapper for the `npm` package to interact with the BugSnag CLI [161](https://github.com/bugsnag/bugsnag-cli/pull/161) diff --git a/install.sh b/install.sh index fa999013..622dc931 100755 --- a/install.sh +++ b/install.sh @@ -91,7 +91,7 @@ display_help() { EOS } -VERSION="2.8.0" +VERSION="2.9.0" while [[ "$#" -gt 0 ]]; do case "$1" in diff --git a/main.go b/main.go index 784b0d49..8729c470 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( "github.com/bugsnag/bugsnag-cli/pkg/utils" ) -var package_version = "2.8.0" +var package_version = "2.9.0" func main() { commands := options.CLI{} diff --git a/package.json b/package.json index 9a3c11a4..8b31c3bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bugsnag/cli", - "version": "2.8.0", + "version": "2.9.0", "description": "BugSnag CLI", "main": "bugsnag-cli-wrapper.js", "bin": { From e826e325f29bef61f4c211cfa39bebe21e692113 Mon Sep 17 00:00:00 2001 From: Josh <46817760+joshedney@users.noreply.github.com> Date: Mon, 20 Jan 2025 14:39:29 +0000 Subject: [PATCH 6/6] Update CHANGELOG.md Co-authored-by: Tom Longridge --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8fdb236..e7dcac91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,6 @@ - Add a wrapper for the `npm` package to interact with the BugSnag CLI [161](https://github.com/bugsnag/bugsnag-cli/pull/161) - Add the support for .so.* files when processing ndk symbol files [163](https://github.com/bugsnag/bugsnag-cli/pull/163) - Add additional logging to the Android AAB upload command [165](https://github.com/bugsnag/bugsnag-cli/pull/165) -- ## 2.8.0 (2025-01-06) ### Enhancements