├── README.md ├── .gitignore ├── autocodesign ├── devportalclient │ ├── spaceship │ │ ├── spaceship │ │ │ ├── Gemfile │ │ │ ├── portal │ │ │ │ ├── common.rb │ │ │ │ ├── auth_client.rb │ │ │ │ └── certificate_client.rb │ │ │ ├── certificates.rb │ │ │ ├── log.rb │ │ │ ├── app.rb │ │ │ ├── devices.rb │ │ │ ├── main.rb │ │ │ └── profiles.rb │ │ ├── auth.go │ │ ├── spaceship_test.go │ │ ├── certificates.go │ │ └── devices.go │ ├── appstoreconnect │ │ ├── provisioning.go │ │ ├── jwt.go │ │ ├── roundtripper.go │ │ ├── error.go │ │ ├── tracker.go │ │ └── roundtripper_test.go │ ├── appstoreconnectclient │ │ ├── auth.go │ │ ├── client.go │ │ └── devices_test.go │ └── time │ │ ├── time_test.go │ │ └── time.go ├── localcodesignasset │ ├── profileprovider.go │ ├── utils.go │ ├── profileconverter.go │ ├── mocks │ │ ├── ProvisioningProfileProvider.go │ │ └── ProvisioningProfileConverter.go │ └── profile.go ├── mock_CertificateProvider.go ├── mock_AssetWriter.go ├── mock_LocalCodeSignAssetManager.go ├── models.go ├── profiledownloader │ └── profiledownloader.go ├── errors.go ├── projectmanager │ └── projectmanager_test.go ├── utils.go ├── certdownloader │ ├── certdownloader.go │ └── certdownloader_test.go ├── entitlement_test.go ├── mock_Profile.go └── utils_test.go ├── renovate.json ├── artifacts ├── zip_reader.go ├── ios_xcarchive_reader.go ├── xcarchive_reader.go └── ipa_reader.go ├── xcodebuild └── xcodebuild.go ├── devportalservice ├── errors.go ├── validated_credentials.go ├── testdevice_test.go ├── testdevice.go └── mock_filemanager.go ├── xcarchive ├── plistutil.go ├── xcarchive_test.go ├── xcarchive.go ├── macos_test.go └── utils.go ├── main.go ├── .golangci.yml ├── xcodecommand ├── xcodecommand.go ├── xcodebuild_only.go ├── xcpretty_installer.go ├── xcpretty_test.go ├── mock_xcprettyManager.go └── xcbeautify.go ├── exportoptionsgenerator ├── plistconverter.go ├── certificates.go ├── archive_info_provider.go ├── mocks │ └── Reader.go ├── profiles.go └── targets.go ├── bitrise.yml ├── _integration_tests ├── appstoreconnect_tests │ ├── appstoreconnect_test.go │ └── api_key_credential_helper.go ├── bucketaccessor.go ├── secretaccessor.go ├── test_helper.go └── zip │ └── ipa_reader_test.go ├── logio ├── pipe_wiring_test.go ├── prefix_filter_benchmark_test.go ├── sink.go ├── pipe_wiring.go └── prefix_filter_test.go ├── mocks ├── Factory.go ├── PathModifier.go ├── PathProvider.go ├── PathChecker.go ├── Command.go └── CommandFactory.go ├── LICENSE ├── destination ├── simulator.go ├── errors.go ├── xcode_runtime_support.go ├── xcode_runtime_support_test.go ├── destination.go └── simulator_test.go ├── xcodeversion ├── xcodeversion.go └── utility.go ├── metaparser ├── metaparser.go ├── xcarchive.go └── ipa.go ├── errorfinder ├── nserror.go └── errorfinder.go ├── internal └── zip │ ├── stdlib_reader.go │ └── ditto_reader.go ├── xcodecache ├── derived_data_path.go ├── derived_data_path_test.go └── swiftpm_cache.go ├── codesign └── mocks │ └── DetailsProvider.go ├── xcconfig ├── xcconfig.go └── xcconfig_test.go ├── zip └── default_reader.go └── go.mod /README.md: -------------------------------------------------------------------------------- 1 | # go-xcode 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _tmp/ 2 | .vscode/* 3 | .idea/* 4 | **/.idea/* 5 | .DS_Store 6 | .env 7 | **/vendor/* 8 | go-xcode 9 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/spaceship/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'fastlane' 4 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/appstoreconnect/provisioning.go: -------------------------------------------------------------------------------- 1 | package appstoreconnect 2 | 3 | // ProvisioningService ... 4 | type ProvisioningService service 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>bitrise-io/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /artifacts/zip_reader.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | // ZipReadCloser ... 4 | type ZipReadCloser interface { 5 | ReadFile(relPthPattern string) ([]byte, error) 6 | Close() error 7 | } 8 | -------------------------------------------------------------------------------- /xcodebuild/xcodebuild.go: -------------------------------------------------------------------------------- 1 | package xcodebuild 2 | 3 | import "github.com/bitrise-io/go-utils/v2/command" 4 | 5 | // CommandModel ... 6 | type CommandModel interface { 7 | PrintableCmd() string 8 | Command(opts *command.Opts) command.Command 9 | } 10 | -------------------------------------------------------------------------------- /devportalservice/errors.go: -------------------------------------------------------------------------------- 1 | package devportalservice 2 | 3 | import "fmt" 4 | 5 | // NetworkError represents a networking issue. 6 | type NetworkError struct { 7 | Status int 8 | } 9 | 10 | func (e NetworkError) Error() string { 11 | return fmt.Sprintf("network request failed with status %d", e.Status) 12 | } 13 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/appstoreconnectclient/auth.go: -------------------------------------------------------------------------------- 1 | package appstoreconnectclient 2 | 3 | // AuthClient ... 4 | type AuthClient struct { 5 | } 6 | 7 | // NewAuthClient ... 8 | func NewAuthClient() *AuthClient { 9 | return &AuthClient{} 10 | } 11 | 12 | // Login ... 13 | func (c *AuthClient) Login() error { 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/spaceship/portal/common.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship' 2 | 3 | def preferred_error_message(ex) 4 | ex.preferred_error_info&.join(' ') || ex.to_s 5 | end 6 | 7 | def run_or_raise_preferred_error_message 8 | yield 9 | rescue Spaceship::Client::UnexpectedResponse => ex 10 | raise preferred_error_message(ex) 11 | end 12 | -------------------------------------------------------------------------------- /xcarchive/plistutil.go: -------------------------------------------------------------------------------- 1 | package xcarchive 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/bitrise-io/go-xcode/v2/plistutil" 7 | ) 8 | 9 | func newPlistDataFromFile(plistPth string) (plistutil.PlistData, error) { 10 | content, err := os.ReadFile(plistPth) 11 | if err != nil { 12 | return plistutil.PlistData{}, err 13 | } 14 | return plistutil.NewPlistDataFromContent(string(content)) 15 | } 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This is a dummy main.go file to import dependencies used in the integration tests (./_integration_tests). 4 | import ( 5 | _ "cloud.google.com/go/secretmanager/apiv1" 6 | _ "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" 7 | _ "cloud.google.com/go/storage" 8 | _ "golang.org/x/oauth2/google" 9 | _ "golang.org/x/oauth2/jwt" 10 | _ "google.golang.org/api/option" 11 | ) 12 | 13 | func main() { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | skip-dirs: 3 | - .*/mocks 4 | 5 | issues: 6 | # https://github.com/golangci/golangci-lint/issues/2439 7 | exclude-use-default: false 8 | 9 | linters: 10 | enable: 11 | - errcheck 12 | - gosimple 13 | - govet 14 | - ineffassign 15 | - staticcheck 16 | - typecheck 17 | - unused 18 | - revive 19 | 20 | linters-settings: 21 | revive: 22 | severity: error 23 | rules: 24 | - name: exported 25 | arguments: 26 | - checkPrivateReceivers 27 | -------------------------------------------------------------------------------- /xcodecommand/xcodecommand.go: -------------------------------------------------------------------------------- 1 | package xcodecommand 2 | 3 | import ( 4 | "github.com/hashicorp/go-version" 5 | ) 6 | 7 | // Output is the direct output of the xcodebuild command, unchanged by log formatters 8 | type Output struct { 9 | RawOut []byte 10 | ExitCode int 11 | } 12 | 13 | // Runner abstarcts an xcodebuild command runner, it can use any log formatter 14 | type Runner interface { 15 | CheckInstall() (*version.Version, error) 16 | Run(workDir string, xcodebuildOpts []string, logFormatterOpts []string) (Output, error) 17 | } 18 | -------------------------------------------------------------------------------- /exportoptionsgenerator/plistconverter.go: -------------------------------------------------------------------------------- 1 | package exportoptionsgenerator 2 | 3 | import ( 4 | plistutilv1 "github.com/bitrise-io/go-xcode/plistutil" 5 | "github.com/bitrise-io/go-xcode/v2/plistutil" 6 | ) 7 | 8 | // TODO: remove this function when export package is migrated to v2 and uses plistutil/v2 9 | func convertToV1PlistData(bundleIDEntitlementsMap map[string]plistutil.PlistData) map[string]plistutilv1.PlistData { 10 | converted := map[string]plistutilv1.PlistData{} 11 | for bundleID, entitlements := range bundleIDEntitlementsMap { 12 | converted[bundleID] = plistutilv1.PlistData(entitlements) 13 | } 14 | return converted 15 | } 16 | -------------------------------------------------------------------------------- /artifacts/ios_xcarchive_reader.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | "github.com/bitrise-io/go-xcode/v2/plistutil" 5 | ) 6 | 7 | // IOSXCArchiveReader ... 8 | type IOSXCArchiveReader struct { 9 | zipReader ZipReadCloser 10 | } 11 | 12 | // NewIOSXCArchiveReader ... 13 | func NewIOSXCArchiveReader(reader ZipReadCloser) IOSXCArchiveReader { 14 | return IOSXCArchiveReader{zipReader: reader} 15 | } 16 | 17 | // AppInfoPlist ... 18 | func (reader IOSXCArchiveReader) AppInfoPlist() (plistutil.PlistData, error) { 19 | b, err := reader.zipReader.ReadFile("*.xcarchive/Products/Applications/*.app/Info.plist") 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return plistutil.NewPlistDataFromContent(string(b)) 25 | } 26 | -------------------------------------------------------------------------------- /bitrise.yml: -------------------------------------------------------------------------------- 1 | format_version: "11" 2 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git 3 | 4 | workflows: 5 | check: 6 | before_run: 7 | - test 8 | - integration_test 9 | 10 | test: 11 | steps: 12 | - git::https://github.com/bitrise-steplib/steps-check.git: 13 | title: Lint 14 | inputs: 15 | - workflow: lint 16 | - skip_step_yml_validation: "yes" 17 | - go-list: 18 | inputs: 19 | - exclude: "*/mocks" 20 | - go-test: { } 21 | 22 | integration_test: 23 | steps: 24 | - change-workdir: 25 | inputs: 26 | - path: ./_integration_tests 27 | - go-list: { } 28 | - go-test: { } 29 | - change-workdir: 30 | inputs: 31 | - path: .. 32 | -------------------------------------------------------------------------------- /_integration_tests/appstoreconnect_tests/appstoreconnect_test.go: -------------------------------------------------------------------------------- 1 | package appstoreconnect_tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestListBundleIDs(t *testing.T) { 11 | keyID, issuerID, privateKey, enterpriseAccount := getAPIKey(t) 12 | 13 | tracker := appstoreconnect.NoOpAnalyticsTracker{} 14 | client := appstoreconnect.NewClient(appstoreconnect.NewRetryableHTTPClient(tracker), keyID, issuerID, []byte(privateKey), enterpriseAccount, tracker) 15 | 16 | response, err := client.Provisioning.ListBundleIDs(&appstoreconnect.ListBundleIDsOptions{}) 17 | require.NoError(t, err) 18 | require.True(t, len(response.Data) > 0) 19 | } 20 | -------------------------------------------------------------------------------- /logio/pipe_wiring_test.go: -------------------------------------------------------------------------------- 1 | package logio_test 2 | 3 | import ( 4 | "io" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/bitrise-io/go-xcode/v2/logio" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestPipeWiring(t *testing.T) { 13 | sut := logio.SetupPipeWiring(regexp.MustCompile(`^\[Bitrise.*\].*`)) 14 | 15 | out := NewChanWriterCloser() 16 | go func() { 17 | _, _ = io.Copy(out, sut.ToolStdin) 18 | _ = out.Close() 19 | }() 20 | 21 | _, _ = sut.XcbuildStdout.Write([]byte(msg1)) 22 | _, _ = sut.XcbuildStdout.Write([]byte(msg2)) 23 | _, _ = sut.XcbuildStdout.Write([]byte(msg3)) 24 | _, _ = sut.XcbuildStdout.Write([]byte(msg4)) 25 | 26 | _ = sut.Close() 27 | 28 | assert.Equal(t, msg1+msg4, sut.XcbuildRawout.String()) 29 | assert.Equal(t, msg1+msg4, out.Messages()) 30 | } 31 | -------------------------------------------------------------------------------- /autocodesign/localcodesignasset/profileprovider.go: -------------------------------------------------------------------------------- 1 | package localcodesignasset 2 | 3 | import "github.com/bitrise-io/go-xcode/profileutil" 4 | 5 | // ProvisioningProfileProvider can list profile infos. 6 | type ProvisioningProfileProvider interface { 7 | ListProvisioningProfiles() ([]profileutil.ProvisioningProfileInfoModel, error) 8 | } 9 | 10 | type provisioningProfileProvider struct{} 11 | 12 | // NewProvisioningProfileProvider ... 13 | func NewProvisioningProfileProvider() ProvisioningProfileProvider { 14 | return provisioningProfileProvider{} 15 | } 16 | 17 | // ListProvisioningProfiles ... 18 | func (p provisioningProfileProvider) ListProvisioningProfiles() ([]profileutil.ProvisioningProfileInfoModel, error) { 19 | return profileutil.InstalledProvisioningProfileInfos(profileutil.ProfileTypeIos) 20 | } 21 | -------------------------------------------------------------------------------- /autocodesign/localcodesignasset/utils.go: -------------------------------------------------------------------------------- 1 | package localcodesignasset 2 | 3 | import ( 4 | "github.com/bitrise-io/go-xcode/v2/autocodesign" 5 | "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" 6 | ) 7 | 8 | func certificateSerials(certsByType map[appstoreconnect.CertificateType][]autocodesign.Certificate, distrType autocodesign.DistributionType) []string { 9 | certType := autocodesign.CertificateTypeByDistribution[distrType] 10 | certs := certsByType[certType] 11 | 12 | var serials []string 13 | for _, cert := range certs { 14 | serials = append(serials, cert.CertificateInfo.Serial) 15 | } 16 | 17 | return serials 18 | } 19 | 20 | func contains(array []string, element string) bool { 21 | for _, item := range array { 22 | if item == element { 23 | return true 24 | } 25 | } 26 | return false 27 | } 28 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/auth.go: -------------------------------------------------------------------------------- 1 | package spaceship 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // AuthClient ... 9 | type AuthClient struct { 10 | client *Client 11 | } 12 | 13 | // NewAuthClient ... 14 | func NewAuthClient(client *Client) *AuthClient { 15 | return &AuthClient{client: client} 16 | } 17 | 18 | // Login ... 19 | func (c *AuthClient) Login() error { 20 | output, err := c.client.runSpaceshipCommand("login") 21 | if err != nil { 22 | return fmt.Errorf("running command failed with error: %w", err) 23 | } 24 | 25 | var teamIDResponse struct { 26 | Data string `json:"data"` 27 | } 28 | if err := json.Unmarshal([]byte(output), &teamIDResponse); err != nil { 29 | return fmt.Errorf("failed to unmarshal response: %w", err) 30 | } 31 | 32 | c.client.teamID = teamIDResponse.Data 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /mocks/Factory.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.10.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | command "github.com/bitrise-io/go-utils/v2/command" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // CommandFactory is an autogenerated mock type for the Factory type 11 | type CommandFactory struct { 12 | mock.Mock 13 | } 14 | 15 | // Create provides a mock function with given fields: name, args, opts 16 | func (_m *CommandFactory) Create(name string, args []string, opts *command.Opts) command.Command { 17 | ret := _m.Called(name, args, opts) 18 | 19 | var r0 command.Command 20 | if rf, ok := ret.Get(0).(func(string, []string, *command.Opts) command.Command); ok { 21 | r0 = rf(name, args, opts) 22 | } else { 23 | if ret.Get(0) != nil { 24 | r0 = ret.Get(0).(command.Command) 25 | } 26 | } 27 | 28 | return r0 29 | } 30 | -------------------------------------------------------------------------------- /exportoptionsgenerator/certificates.go: -------------------------------------------------------------------------------- 1 | package exportoptionsgenerator 2 | 3 | import "github.com/bitrise-io/go-xcode/certificateutil" 4 | 5 | // CodesignIdentityProvider can list certificate infos. 6 | type CodesignIdentityProvider interface { 7 | ListCodesignIdentities() ([]certificateutil.CertificateInfoModel, error) 8 | } 9 | 10 | // LocalCodesignIdentityProvider ... 11 | type LocalCodesignIdentityProvider struct{} 12 | 13 | // ListCodesignIdentities ... 14 | func (p LocalCodesignIdentityProvider) ListCodesignIdentities() ([]certificateutil.CertificateInfoModel, error) { 15 | certs, err := certificateutil.InstalledCodesigningCertificateInfos() 16 | if err != nil { 17 | return nil, err 18 | } 19 | certInfo := certificateutil.FilterValidCertificateInfos(certs) 20 | return append(certInfo.ValidCertificates, certInfo.DuplicatedCertificates...), nil 21 | } 22 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/spaceship/certificates.rb: -------------------------------------------------------------------------------- 1 | require_relative 'portal/certificate_client' 2 | 3 | # CertificateHelper ... 4 | class CertificateHelper 5 | def list_dev_certs 6 | certs = Portal::CertificateClient.download_development_certificates 7 | get_cert_infos(certs) 8 | end 9 | 10 | def list_dist_certs 11 | get_cert_infos(Portal::CertificateClient.download_production_certificates) 12 | end 13 | 14 | def get_cert_infos(portal_certificates) 15 | cert_infos = [] 16 | portal_certificates.each do |cert| 17 | downloaded_portal_cert = cert.download 18 | base64_pem = Base64.encode64(downloaded_portal_cert.to_pem) 19 | 20 | cert_info = { 21 | content: base64_pem, 22 | id: cert.id 23 | } 24 | 25 | cert_infos.append(cert_info) 26 | end 27 | 28 | cert_infos 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /devportalservice/validated_credentials.go: -------------------------------------------------------------------------------- 1 | package devportalservice 2 | 3 | // Credentials contains only one of Apple ID (the session cookies already checked) or APIKey auth info 4 | type Credentials struct { 5 | AppleID *AppleID 6 | APIKey *APIKeyConnection 7 | } 8 | 9 | // AppleID contains Apple ID auth info 10 | // 11 | // Without 2FA: 12 | // 13 | // Required: username, password 14 | // 15 | // With 2FA: 16 | // 17 | // Required: username, password, appSpecificPassword 18 | // session (Only for Fastlane, set as FASTLANE_SESSION) 19 | // 20 | // As Fastlane spaceship uses: 21 | // - iTMSTransporter: it requires Username + Password (or App-specific password with 2FA) 22 | // - TunesAPI: it requires Username + Password (+ 2FA session with 2FA) 23 | type AppleID struct { 24 | Username, Password string 25 | Session, AppSpecificPassword string 26 | } 27 | -------------------------------------------------------------------------------- /artifacts/xcarchive_reader.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | "github.com/bitrise-io/go-xcode/v2/plistutil" 5 | ) 6 | 7 | // XCArchiveReader ... 8 | type XCArchiveReader struct { 9 | zipReader ZipReadCloser 10 | } 11 | 12 | // NewXCArchiveReader ... 13 | func NewXCArchiveReader(reader ZipReadCloser) XCArchiveReader { 14 | return XCArchiveReader{zipReader: reader} 15 | } 16 | 17 | // InfoPlist ... 18 | func (reader XCArchiveReader) InfoPlist() (plistutil.PlistData, error) { 19 | b, err := reader.zipReader.ReadFile("*.xcarchive/Info.plist") 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return plistutil.NewPlistDataFromContent(string(b)) 25 | } 26 | 27 | // IsMacOS ... 28 | func (reader XCArchiveReader) IsMacOS() bool { 29 | _, err := reader.zipReader.ReadFile("*.xcarchive/Products/Applications/*.app/Contents/Info.plist") 30 | return err == nil 31 | } 32 | -------------------------------------------------------------------------------- /exportoptionsgenerator/archive_info_provider.go: -------------------------------------------------------------------------------- 1 | package exportoptionsgenerator 2 | 3 | import ( 4 | "github.com/bitrise-io/go-xcode/v2/xcarchive" 5 | ) 6 | 7 | // ExportProduct ... 8 | type ExportProduct string 9 | 10 | const ( 11 | // ExportProductApp ... 12 | ExportProductApp ExportProduct = "app" 13 | // ExportProductAppClip ... 14 | ExportProductAppClip ExportProduct = "app-clip" 15 | ) 16 | 17 | // ReadArchiveExportInfo ... 18 | func ReadArchiveExportInfo(archive xcarchive.IosArchive) (ArchiveInfo, error) { 19 | appClipBundleID := "" 20 | if archive.Application.ClipApplication != nil { 21 | appClipBundleID = archive.Application.ClipApplication.BundleIdentifier() 22 | } 23 | 24 | return ArchiveInfo{ 25 | AppBundleID: archive.Application.BundleIdentifier(), 26 | AppClipBundleID: appClipBundleID, 27 | EntitlementsByBundleID: archive.BundleIDEntitlementsMap(), 28 | }, nil 29 | } 30 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/appstoreconnectclient/client.go: -------------------------------------------------------------------------------- 1 | // Package appstoreconnectclient implements autocodesign.DevPortalClient, using an API key as the authentication method. 2 | // 3 | // It depends on appstoreconnect package. 4 | package appstoreconnectclient 5 | 6 | import ( 7 | "github.com/bitrise-io/go-xcode/v2/autocodesign" 8 | "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" 9 | ) 10 | 11 | // Client ... 12 | type Client struct { 13 | *AuthClient 14 | *CertificateSource 15 | *DeviceClient 16 | *ProfileClient 17 | } 18 | 19 | // NewAPIDevPortalClient ... 20 | func NewAPIDevPortalClient(client *appstoreconnect.Client) autocodesign.DevPortalClient { 21 | return Client{ 22 | AuthClient: NewAuthClient(), 23 | CertificateSource: NewCertificateSource(client), 24 | DeviceClient: NewDeviceClient(client), 25 | ProfileClient: NewProfileClient(client), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/spaceship/log.rb: -------------------------------------------------------------------------------- 1 | # Log 2 | class Log 3 | @verbose = true 4 | 5 | class << self 6 | attr_accessor :verbose 7 | end 8 | 9 | def self.info(str) 10 | puts("\n\e[34m#{str}\e[0m") 11 | end 12 | 13 | def self.print(str) 14 | puts(str.to_s) 15 | end 16 | 17 | def self.success(str) 18 | puts("\e[32m#{str}\e[0m") 19 | end 20 | 21 | def self.warn(str) 22 | puts("\e[33m#{str}\e[0m") 23 | end 24 | 25 | def self.error(str) 26 | puts("\e[31m#{str}\e[0m") 27 | end 28 | 29 | def self.debug(str) 30 | puts("\e[90m#{str}\e[0m") if @verbose 31 | end 32 | 33 | def self.debug_exception(exc) 34 | Log.debug('Error:') 35 | Log.debug(exc.to_s) 36 | puts 37 | Log.debug('Stacktrace (for debugging):') 38 | Log.debug(exc.backtrace.join("\n").to_s) 39 | end 40 | 41 | def self.secure_value(value) 42 | return '' if value.empty? 43 | '***' 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /logio/prefix_filter_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package logio_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "regexp" 9 | "sync" 10 | "testing" 11 | 12 | "github.com/bitrise-io/go-xcode/v2/logio" 13 | ) 14 | 15 | func BenchmarkPrefixFilterWithMultiWriter(b *testing.B) { 16 | re := regexp.MustCompile(`^\[Bitrise.*\].*`) 17 | 18 | var wg sync.WaitGroup 19 | wg.Add(1) 20 | var buildOutBuffer bytes.Buffer 21 | stdout := logio.NewSink(os.Stdout) 22 | var buildOutWriter = logio.NewSink(io.MultiWriter(&buildOutBuffer, stdout)) 23 | sut := logio.NewPrefixFilter( 24 | re, 25 | stdout, 26 | buildOutWriter, 27 | ) 28 | 29 | for i := 0; i < b.N; i++ { 30 | b.StartTimer() 31 | _, _ = sut.Write(fmt.Append(nil, "Log message without prefix: ", i, "\n")) 32 | _, _ = sut.Write(fmt.Append(nil, "[Bitrise Analytics] Log message with prefix: ", i, "\n")) 33 | b.StopTimer() 34 | 35 | select { 36 | case err := <-sut.MessageLost(): 37 | fmt.Printf("Failed on %d: %s", i, err.Error()) 38 | b.FailNow() 39 | default: 40 | } 41 | } 42 | wg.Done() 43 | } 44 | -------------------------------------------------------------------------------- /autocodesign/localcodesignasset/profileconverter.go: -------------------------------------------------------------------------------- 1 | package localcodesignasset 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/bitrise-io/go-xcode/profileutil" 7 | "github.com/bitrise-io/go-xcode/v2/autocodesign" 8 | ) 9 | 10 | // ProvisioningProfileConverter ... 11 | type ProvisioningProfileConverter interface { 12 | ProfileInfoToProfile(info profileutil.ProvisioningProfileInfoModel) (autocodesign.Profile, error) 13 | } 14 | 15 | type provisioningProfileConverter struct { 16 | } 17 | 18 | // NewProvisioningProfileConverter ... 19 | func NewProvisioningProfileConverter() ProvisioningProfileConverter { 20 | return provisioningProfileConverter{} 21 | } 22 | 23 | // ProfileInfoToProfile ... 24 | func (c provisioningProfileConverter) ProfileInfoToProfile(info profileutil.ProvisioningProfileInfoModel) (autocodesign.Profile, error) { 25 | _, pth, err := profileutil.FindProvisioningProfile(info.UUID) 26 | if err != nil { 27 | return nil, err 28 | } 29 | content, err := os.ReadFile(pth) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return NewProfile(info, content), nil 35 | } 36 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/spaceship/portal/auth_client.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship' 2 | 3 | module Portal 4 | # AuthClient ... 5 | class AuthClient 6 | def self.login(username, password, two_factor_session = nil, team_id = nil) 7 | ENV['FASTLANE_SESSION'] = two_factor_session unless two_factor_session.to_s.empty? 8 | ENV['SPACESHIP_SKIP_2FA_UPGRADE'] = '1' 9 | 10 | client = Spaceship::PortalClient.login(username, password) 11 | 12 | if team_id.to_s.empty? 13 | teams = client.teams 14 | raise 'Your developer portal account belongs to multiple teams, please provide the team id to sign in' if teams.to_a.size > 1 15 | else 16 | client.team_id = team_id 17 | end 18 | 19 | client.store_cookie 20 | 21 | client.team_id 22 | end 23 | 24 | def self.restore_from_session(username, team_id) 25 | client = Spaceship::PortalClient.new(current_team_id: team_id) 26 | client.user = username 27 | client.load_session_from_file 28 | 29 | Spaceship::Portal.client = client 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /autocodesign/mock_CertificateProvider.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery 2.9.4. DO NOT EDIT. 2 | 3 | package autocodesign 4 | 5 | import ( 6 | certificateutil "github.com/bitrise-io/go-xcode/certificateutil" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // MockCertificateProvider is an autogenerated mock type for the CertificateProvider type 11 | type MockCertificateProvider struct { 12 | mock.Mock 13 | } 14 | 15 | // GetCertificates provides a mock function with given fields: 16 | func (_m *MockCertificateProvider) GetCertificates() ([]certificateutil.CertificateInfoModel, error) { 17 | ret := _m.Called() 18 | 19 | var r0 []certificateutil.CertificateInfoModel 20 | if rf, ok := ret.Get(0).(func() []certificateutil.CertificateInfoModel); ok { 21 | r0 = rf() 22 | } else { 23 | if ret.Get(0) != nil { 24 | r0, ok = ret.Get(0).([]certificateutil.CertificateInfoModel) 25 | if !ok { 26 | } 27 | } 28 | } 29 | 30 | var r1 error 31 | if rf, ok := ret.Get(1).(func() error); ok { 32 | r1 = rf() 33 | } else { 34 | r1 = ret.Error(1) 35 | } 36 | 37 | return r0, r1 38 | } 39 | -------------------------------------------------------------------------------- /autocodesign/localcodesignasset/mocks/ProvisioningProfileProvider.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery 2.9.4. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | profileutil "github.com/bitrise-io/go-xcode/profileutil" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // ProvisioningProfileProvider is an autogenerated mock type for the ProvisioningProfileProvider type 11 | type ProvisioningProfileProvider struct { 12 | mock.Mock 13 | } 14 | 15 | // ListProvisioningProfiles provides a mock function with given fields: 16 | func (_m *ProvisioningProfileProvider) ListProvisioningProfiles() ([]profileutil.ProvisioningProfileInfoModel, error) { 17 | ret := _m.Called() 18 | 19 | var r0 []profileutil.ProvisioningProfileInfoModel 20 | if rf, ok := ret.Get(0).(func() []profileutil.ProvisioningProfileInfoModel); ok { 21 | r0 = rf() 22 | } else { 23 | if ret.Get(0) != nil { 24 | r0 = ret.Get(0).([]profileutil.ProvisioningProfileInfoModel) 25 | } 26 | } 27 | 28 | var r1 error 29 | if rf, ok := ret.Get(1).(func() error); ok { 30 | r1 = rf() 31 | } else { 32 | r1 = ret.Error(1) 33 | } 34 | 35 | return r0, r1 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bitrise 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /destination/simulator.go: -------------------------------------------------------------------------------- 1 | package destination 2 | 3 | import "fmt" 4 | 5 | // Simulator ... 6 | type Simulator struct { 7 | Platform string 8 | Name string 9 | OS string 10 | Arch string 11 | } 12 | 13 | // NewSimulator ... 14 | func NewSimulator(destination string) (*Simulator, error) { 15 | specifier, err := NewSpecifier(destination) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | platform, isGeneric := specifier.Platform() 21 | if isGeneric { 22 | return nil, fmt.Errorf("can't create a simulator from generic destination: %s", destination) 23 | } 24 | 25 | simulator := Simulator{ 26 | Platform: string(platform), 27 | Name: specifier.Name(), 28 | OS: specifier.OS(), 29 | Arch: specifier.Arch(), 30 | } 31 | 32 | if simulator.Platform == "" { 33 | return nil, fmt.Errorf(`missing key "platform" in destination: %s`, destination) 34 | } 35 | 36 | if simulator.Name == "" { 37 | return nil, fmt.Errorf(`missing key "name" in destination: %s`, destination) 38 | } 39 | 40 | if simulator.OS == "" { 41 | // OS=latest can be omitted in the destination specifier, because it's the default value. 42 | simulator.OS = "latest" 43 | } 44 | 45 | return &simulator, nil 46 | } 47 | -------------------------------------------------------------------------------- /autocodesign/localcodesignasset/mocks/ProvisioningProfileConverter.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery 2.9.4. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | autocodesign "github.com/bitrise-io/go-xcode/v2/autocodesign" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | 10 | profileutil "github.com/bitrise-io/go-xcode/profileutil" 11 | ) 12 | 13 | // ProvisioningProfileConverter is an autogenerated mock type for the ProvisioningProfileConverter type 14 | type ProvisioningProfileConverter struct { 15 | mock.Mock 16 | } 17 | 18 | // ProfileInfoToProfile provides a mock function with given fields: info 19 | func (_m *ProvisioningProfileConverter) ProfileInfoToProfile(info profileutil.ProvisioningProfileInfoModel) (autocodesign.Profile, error) { 20 | ret := _m.Called(info) 21 | 22 | var r0 autocodesign.Profile 23 | if rf, ok := ret.Get(0).(func(profileutil.ProvisioningProfileInfoModel) autocodesign.Profile); ok { 24 | r0 = rf(info) 25 | } else { 26 | if ret.Get(0) != nil { 27 | r0 = ret.Get(0).(autocodesign.Profile) 28 | } 29 | } 30 | 31 | var r1 error 32 | if rf, ok := ret.Get(1).(func(profileutil.ProvisioningProfileInfoModel) error); ok { 33 | r1 = rf(info) 34 | } else { 35 | r1 = ret.Error(1) 36 | } 37 | 38 | return r0, r1 39 | } 40 | -------------------------------------------------------------------------------- /destination/errors.go: -------------------------------------------------------------------------------- 1 | package destination 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // missingDeviceErr is raised when the selected runtime is available, but the device doesn't exist for that runtime. 9 | // We can recover from this error by creating the given device. 10 | type missingDeviceErr struct { 11 | name, deviceTypeID, runtimeID string 12 | } 13 | 14 | func newMissingDeviceErr(name, deviceTypeID, runtimeID string) *missingDeviceErr { 15 | return &missingDeviceErr{ 16 | name: name, 17 | deviceTypeID: deviceTypeID, 18 | runtimeID: runtimeID, 19 | } 20 | } 21 | 22 | func (e *missingDeviceErr) Error() string { 23 | return fmt.Sprintf("device (%s) with runtime (%s) is not yet created", e.name, e.runtimeID) 24 | } 25 | 26 | func newMissingRuntimeErr(platform, version string, availableRuntimes []DeviceRuntime) error { 27 | runtimeList := prettyRuntimeList(availableRuntimes) 28 | return fmt.Errorf("%s %s is not installed. Choose one of the available %s runtimes: \n%s", platform, version, platform, runtimeList) 29 | } 30 | 31 | func prettyRuntimeList(runtimes []DeviceRuntime) string { 32 | var items []string 33 | for _, runtime := range runtimes { 34 | items = append(items, fmt.Sprintf("- %s", runtime.Name)) 35 | } 36 | return strings.Join(items, "\n") 37 | 38 | } 39 | -------------------------------------------------------------------------------- /devportalservice/testdevice_test.go: -------------------------------------------------------------------------------- 1 | package devportalservice 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | "time" 7 | 8 | "github.com/bitrise-io/go-utils/fileutil" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestDeviceParsing(t *testing.T) { 14 | content := "00000000–0000000000000001,00000000–0000000000000002,00000000–0000000000000003" 15 | pth := path.Join(t.TempDir(), "devices.txt") 16 | err := fileutil.WriteStringToFile(pth, content) 17 | require.NoError(t, err) 18 | 19 | currentTime := time.Now() 20 | got, err := ParseTestDevicesFromFile(pth, currentTime) 21 | require.NoError(t, err) 22 | 23 | expected := []TestDevice{ 24 | { 25 | DeviceID: "00000000–0000000000000001", 26 | Title: "Device 1", 27 | CreatedAt: currentTime, 28 | UpdatedAt: currentTime, 29 | DeviceType: "unknown", 30 | }, 31 | { 32 | DeviceID: "00000000–0000000000000002", 33 | Title: "Device 2", 34 | CreatedAt: currentTime, 35 | UpdatedAt: currentTime, 36 | DeviceType: "unknown", 37 | }, 38 | { 39 | DeviceID: "00000000–0000000000000003", 40 | Title: "Device 3", 41 | CreatedAt: currentTime, 42 | UpdatedAt: currentTime, 43 | DeviceType: "unknown", 44 | }, 45 | } 46 | assert.Equal(t, expected, got) 47 | } 48 | -------------------------------------------------------------------------------- /xcarchive/xcarchive_test.go: -------------------------------------------------------------------------------- 1 | package xcarchive 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/bitrise-io/go-utils/v2/log" 8 | "github.com/bitrise-io/go-utils/v2/pathutil" 9 | ) 10 | 11 | const ( 12 | tempDirName = "__artifacts__" 13 | DSYMSDirName = "dSYMs" 14 | ) 15 | 16 | func TestIsMacOS(t *testing.T) { 17 | tests := []struct { 18 | name string 19 | archPath string 20 | want bool 21 | wantErr bool 22 | }{ 23 | { 24 | name: "macOS", 25 | archPath: filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive"), 26 | want: true, 27 | wantErr: false, 28 | }, 29 | { 30 | name: "iOS", 31 | archPath: filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive"), 32 | want: false, 33 | wantErr: false, 34 | }, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | pathChecker := pathutil.NewPathChecker() 39 | logger := log.NewLogger() 40 | archiveReader := NewArchiveReader(pathChecker, logger) 41 | got, err := archiveReader.IsMacOS(tt.archPath) 42 | if (err != nil) != tt.wantErr { 43 | t.Errorf("IsMacOS() error = %v, wantErr %v", err, tt.wantErr) 44 | return 45 | } 46 | if got != tt.want { 47 | t.Errorf("IsMacOS() = %v, want %v", got, tt.want) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /mocks/PathModifier.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // PathModifier is an autogenerated mock type for the PathModifier type 8 | type PathModifier struct { 9 | mock.Mock 10 | } 11 | 12 | // AbsPath provides a mock function with given fields: pth 13 | func (_m *PathModifier) AbsPath(pth string) (string, error) { 14 | ret := _m.Called(pth) 15 | 16 | var r0 string 17 | var r1 error 18 | if rf, ok := ret.Get(0).(func(string) (string, error)); ok { 19 | return rf(pth) 20 | } 21 | if rf, ok := ret.Get(0).(func(string) string); ok { 22 | r0 = rf(pth) 23 | } else { 24 | r0 = ret.Get(0).(string) 25 | } 26 | 27 | if rf, ok := ret.Get(1).(func(string) error); ok { 28 | r1 = rf(pth) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | 36 | // NewPathModifier creates a new instance of PathModifier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 37 | // The first argument is typically a *testing.T value. 38 | func NewPathModifier(t interface { 39 | mock.TestingT 40 | Cleanup(func()) 41 | }) *PathModifier { 42 | mock := &PathModifier{} 43 | mock.Mock.Test(t) 44 | 45 | t.Cleanup(func() { mock.AssertExpectations(t) }) 46 | 47 | return mock 48 | } 49 | -------------------------------------------------------------------------------- /destination/xcode_runtime_support.go: -------------------------------------------------------------------------------- 1 | package destination 2 | 3 | import ( 4 | "github.com/bitrise-io/go-xcode/v2/xcodeversion" 5 | "github.com/hashicorp/go-version" 6 | ) 7 | 8 | func isRuntimeSupportedByXcode(runtimePlatform string, runtimeVersion *version.Version, xcodeVersion xcodeversion.Version) bool { 9 | // Very simplified version of https://developer.apple.com/support/xcode/ 10 | // Only considering major versions for simplicity 11 | var xcodeVersionToSupportedRuntimes = map[int64]map[string]int64{ 12 | 15: { 13 | string(IOS): 17, 14 | string(TvOS): 17, 15 | string(WatchOS): 10, 16 | }, 17 | 14: { 18 | string(IOS): 16, 19 | string(TvOS): 16, 20 | string(WatchOS): 9, 21 | }, 22 | 13: { 23 | string(IOS): 15, 24 | string(TvOS): 15, 25 | string(WatchOS): 8, 26 | }, 27 | } 28 | 29 | if len(runtimeVersion.Segments64()) == 0 || xcodeVersion.Major == 0 { 30 | return true 31 | } 32 | runtimeMajorVersion := runtimeVersion.Segments64()[0] 33 | 34 | platformToLatestSupportedVersion, ok := xcodeVersionToSupportedRuntimes[xcodeVersion.Major] 35 | if !ok { 36 | return true 37 | } 38 | 39 | latestSupportedMajorVersion, ok := platformToLatestSupportedVersion[runtimePlatform] 40 | if !ok { 41 | return true 42 | } 43 | 44 | return latestSupportedMajorVersion >= runtimeMajorVersion 45 | } 46 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/spaceship/app.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship' 2 | require_relative 'portal/app_client' 3 | 4 | def get_app(bundle_id) 5 | app = nil 6 | run_or_raise_preferred_error_message do 7 | app = Spaceship::Portal.app.find(bundle_id) 8 | end 9 | 10 | return [] unless app 11 | 12 | [{ 13 | id: app.app_id, 14 | bundleID: app.bundle_id, 15 | name: app.name, 16 | entitlements: app.details.features 17 | }] 18 | end 19 | 20 | def create_app(bundle_id, bundle_id_name) 21 | app = nil 22 | run_or_raise_preferred_error_message do 23 | app = Spaceship::Portal.app.create!(bundle_id: bundle_id, name: bundle_id_name) 24 | end 25 | 26 | raise "failed to create app with bundle id: #{bundle_id}" unless app 27 | 28 | { 29 | id: app.app_id, 30 | bundleID: app.bundle_id, 31 | name: app.name 32 | } 33 | end 34 | 35 | def check_bundleid(bundle_id, entitlements) 36 | app = nil 37 | run_or_raise_preferred_error_message do 38 | app = Spaceship::Portal.app.find(bundle_id) 39 | end 40 | 41 | Portal::AppClient.all_services_enabled?(app, entitlements) 42 | end 43 | 44 | def sync_bundleid(bundle_id, entitlements) 45 | app = nil 46 | run_or_raise_preferred_error_message do 47 | app = Spaceship::Portal.app.find(bundle_id) 48 | end 49 | 50 | Portal::AppClient.sync_app_services(app, entitlements) 51 | end 52 | -------------------------------------------------------------------------------- /mocks/PathProvider.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // PathProvider is an autogenerated mock type for the PathProvider type 8 | type PathProvider struct { 9 | mock.Mock 10 | } 11 | 12 | // CreateTempDir provides a mock function with given fields: prefix 13 | func (_m *PathProvider) CreateTempDir(prefix string) (string, error) { 14 | ret := _m.Called(prefix) 15 | 16 | var r0 string 17 | var r1 error 18 | if rf, ok := ret.Get(0).(func(string) (string, error)); ok { 19 | return rf(prefix) 20 | } 21 | if rf, ok := ret.Get(0).(func(string) string); ok { 22 | r0 = rf(prefix) 23 | } else { 24 | r0 = ret.Get(0).(string) 25 | } 26 | 27 | if rf, ok := ret.Get(1).(func(string) error); ok { 28 | r1 = rf(prefix) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | 36 | // NewPathProvider creates a new instance of PathProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 37 | // The first argument is typically a *testing.T value. 38 | func NewPathProvider(t interface { 39 | mock.TestingT 40 | Cleanup(func()) 41 | }) *PathProvider { 42 | mock := &PathProvider{} 43 | mock.Mock.Test(t) 44 | 45 | t.Cleanup(func() { mock.AssertExpectations(t) }) 46 | 47 | return mock 48 | } 49 | -------------------------------------------------------------------------------- /devportalservice/testdevice.go: -------------------------------------------------------------------------------- 1 | package devportalservice 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "github.com/bitrise-io/go-utils/pathutil" 10 | ) 11 | 12 | // TestDevice ... 13 | type TestDevice struct { 14 | ID int `json:"id"` 15 | UserID int `json:"user_id"` 16 | // DeviceID is the Apple device UDID 17 | DeviceID string `json:"device_identifier"` 18 | Title string `json:"title"` 19 | CreatedAt time.Time `json:"created_at"` 20 | UpdatedAt time.Time `json:"updated_at"` 21 | DeviceType string `json:"device_type"` 22 | } 23 | 24 | // ParseTestDevicesFromFile ... 25 | func ParseTestDevicesFromFile(path string, currentTime time.Time) ([]TestDevice, error) { 26 | absPath, err := pathutil.AbsPath(path) 27 | if err != nil { 28 | return []TestDevice{}, err 29 | } 30 | 31 | bytes, err := os.ReadFile(absPath) 32 | if err != nil { 33 | return []TestDevice{}, err 34 | } 35 | 36 | fileContent := strings.TrimSpace(string(bytes)) 37 | identifiers := strings.Split(fileContent, ",") 38 | 39 | var testDevices []TestDevice 40 | for i, identifier := range identifiers { 41 | testDevices = append(testDevices, TestDevice{ 42 | DeviceID: identifier, 43 | Title: fmt.Sprintf("Device %d", i+1), 44 | CreatedAt: currentTime, 45 | UpdatedAt: currentTime, 46 | DeviceType: "unknown", 47 | }) 48 | } 49 | 50 | return testDevices, nil 51 | } 52 | -------------------------------------------------------------------------------- /_integration_tests/bucketaccessor.go: -------------------------------------------------------------------------------- 1 | package _integration_tests 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | "time" 8 | 9 | "cloud.google.com/go/storage" 10 | "golang.org/x/oauth2/google" 11 | "golang.org/x/oauth2/jwt" 12 | ) 13 | 14 | // BucketAccessor ... 15 | type BucketAccessor struct { 16 | jwtConfig *jwt.Config 17 | bucket string 18 | objectExpiry time.Duration 19 | } 20 | 21 | // NewBucketAccessor ... 22 | func NewBucketAccessor(serviceAccountJSONContent, bucket string) (*BucketAccessor, error) { 23 | conf, err := google.JWTConfigFromJSON([]byte(serviceAccountJSONContent)) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return &BucketAccessor{ 29 | jwtConfig: conf, 30 | bucket: bucket, 31 | objectExpiry: 1 * time.Hour, 32 | }, nil 33 | } 34 | 35 | // GetExpiringURL ... 36 | func (a BucketAccessor) GetExpiringURL(originalURL string) (string, error) { 37 | artifactPath := strings.TrimPrefix(strings.TrimPrefix(originalURL, fmt.Sprintf("https://storage.googleapis.com/%s/", a.bucket)), fmt.Sprintf("https://storage.cloud.google.com/%s/", a.bucket)) 38 | opts := &storage.SignedURLOptions{ 39 | Method: http.MethodGet, 40 | GoogleAccessID: a.jwtConfig.Email, 41 | PrivateKey: a.jwtConfig.PrivateKey, 42 | Expires: time.Now().Add(a.objectExpiry), 43 | } 44 | 45 | return storage.SignedURL(a.bucket, artifactPath, opts) 46 | } 47 | -------------------------------------------------------------------------------- /artifacts/ipa_reader.go: -------------------------------------------------------------------------------- 1 | package artifacts 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bitrise-io/go-xcode/profileutil" 7 | "github.com/bitrise-io/go-xcode/v2/plistutil" 8 | ) 9 | 10 | // IPAReader ... 11 | type IPAReader struct { 12 | zipReader ZipReadCloser 13 | } 14 | 15 | // NewIPAReader ... 16 | func NewIPAReader(zipReader ZipReadCloser) IPAReader { 17 | return IPAReader{zipReader: zipReader} 18 | } 19 | 20 | // ProvisioningProfileInfo ... 21 | func (reader IPAReader) ProvisioningProfileInfo() (*profileutil.ProvisioningProfileInfoModel, error) { 22 | b, err := reader.zipReader.ReadFile("Payload/*.app/embedded.mobileprovision") 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | profilePKCS7, err := profileutil.ProvisioningProfileFromContent(b) 28 | if err != nil { 29 | return nil, fmt.Errorf("failed to parse embedded.mobilprovision: %w", err) 30 | } 31 | 32 | provisioningProfileInfo, err := profileutil.NewProvisioningProfileInfo(*profilePKCS7) 33 | if err != nil { 34 | return nil, fmt.Errorf("failed to read profile info: %w", err) 35 | } 36 | 37 | return &provisioningProfileInfo, nil 38 | } 39 | 40 | // AppInfoPlist ... 41 | func (reader IPAReader) AppInfoPlist() (plistutil.PlistData, error) { 42 | b, err := reader.zipReader.ReadFile("Payload/*.app/Info.plist") 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return plistutil.NewPlistDataFromContent(string(b)) 48 | } 49 | -------------------------------------------------------------------------------- /xcodeversion/xcodeversion.go: -------------------------------------------------------------------------------- 1 | package xcodeversion 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bitrise-io/go-utils/v2/command" 7 | ) 8 | 9 | // Version ... 10 | type Version struct { 11 | Version string 12 | BuildVersion string 13 | Major int64 14 | Minor int64 15 | } 16 | 17 | // Reader ... 18 | type Reader interface { 19 | GetVersion() (Version, error) 20 | } 21 | 22 | type reader struct { 23 | commandFactory command.Factory 24 | } 25 | 26 | // NewXcodeVersionProvider ... 27 | func NewXcodeVersionProvider(commandFactory command.Factory) Reader { 28 | return &reader{ 29 | commandFactory: commandFactory, 30 | } 31 | } 32 | 33 | // GetVersion ... 34 | func (b *reader) GetVersion() (Version, error) { 35 | cmd := b.commandFactory.Create("xcodebuild", []string{"-version"}, &command.Opts{}) 36 | 37 | outStr, err := cmd.RunAndReturnTrimmedCombinedOutput() 38 | if err != nil { 39 | return Version{}, fmt.Errorf("xcodebuild -version failed: %s, output: %s", err, outStr) 40 | } 41 | 42 | return getXcodeVersionFromXcodebuildOutput(outStr) 43 | } 44 | 45 | // IsGreaterThanOrEqualTo checks if the Xcode version is greater than or equal to the given major and minor version. 46 | func (v Version) IsGreaterThanOrEqualTo(major, minor int64) bool { 47 | if v.Major > major { 48 | return true 49 | } 50 | if v.Major == major && v.Minor >= minor { 51 | return true 52 | } 53 | return false 54 | } 55 | -------------------------------------------------------------------------------- /devportalservice/mock_filemanager.go: -------------------------------------------------------------------------------- 1 | package devportalservice 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strings" 7 | 8 | "github.com/bitrise-io/go-utils/v2/fileutil" 9 | ) 10 | 11 | type mockFileReader struct { 12 | contents string 13 | } 14 | 15 | func newMockFileReader(contents string) fileutil.FileManager { 16 | return &mockFileReader{ 17 | contents: contents, 18 | } 19 | } 20 | 21 | // Open ... 22 | func (r *mockFileReader) Open(path string) (*os.File, error) { 23 | panic("not implemented") 24 | } 25 | 26 | // OpenReaderIfExists ... 27 | func (r *mockFileReader) OpenReaderIfExists(path string) (io.Reader, error) { 28 | return io.NopCloser(strings.NewReader(r.contents)), nil 29 | } 30 | 31 | // ReadDirEntryNames ... 32 | func (r *mockFileReader) ReadDirEntryNames(path string) ([]string, error) { 33 | panic("not implemented") 34 | } 35 | 36 | // Remove ... 37 | func (r *mockFileReader) Remove(path string) error { 38 | panic("not implemented") 39 | } 40 | 41 | // RemoveAll ... 42 | func (r *mockFileReader) RemoveAll(path string) error { 43 | panic("not implemented") 44 | } 45 | 46 | // Write ... 47 | func (r *mockFileReader) Write(path string, value string, perm os.FileMode) error { 48 | panic("not implemented") 49 | } 50 | 51 | // WriteBytes ... 52 | func (r *mockFileReader) WriteBytes(path string, value []byte) error { 53 | panic("not implemented") 54 | } 55 | 56 | // FileSizeInBytes ... 57 | func (r *mockFileReader) FileSizeInBytes(pth string) (int64, error) { 58 | panic("not implemented") 59 | } 60 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/time/time_test.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTime_UnmarshalJSON(t *testing.T) { 8 | 9 | tests := []struct { 10 | b []byte 11 | name string 12 | t *Time 13 | wantErr bool 14 | }{ 15 | {name: "without quotation mark", b: []byte("2021-05-19T08:07:47.000+00:00"), wantErr: false}, 16 | {name: "with quotation mark", b: []byte(`"2021-05-19T08:07:47.000+00:00"`), wantErr: false}, 17 | {name: "Positive offset", b: []byte("2021-05-19T08:07:47.000+05:30"), wantErr: false}, 18 | {name: "Alternate positive offset", b: []byte("2021-05-19T08:07:47.000+0530"), wantErr: false}, 19 | {name: "Alternate negative offset", b: []byte("2021-05-19T08:07:47.000-0400"), wantErr: false}, 20 | {name: "Single hour positive offset", b: []byte("2021-05-19T08:07:47.000+03"), wantErr: false}, 21 | {name: "Single hour negative offset", b: []byte("2021-05-19T08:07:47.000-02"), wantErr: false}, 22 | {name: "Zero offset UTC", b: []byte("2021-05-19T08:07:47.000Z"), wantErr: false}, 23 | {name: "Custom spaceship time format", b: []byte("2022-04-01 12:45:25 UTC"), wantErr: false}, 24 | {name: "unsupported format", b: []byte("2021-12-17T10:44:00Z00:00"), wantErr: true}, 25 | } 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | time := &Time{} 29 | if err := time.UnmarshalJSON(tt.b); (err != nil) != tt.wantErr { 30 | t.Errorf("Time.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) 31 | } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /exportoptionsgenerator/mocks/Reader.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.53.3. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | xcodeversion "github.com/bitrise-io/go-xcode/v2/xcodeversion" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // Reader is an autogenerated mock type for the Reader type 11 | type Reader struct { 12 | mock.Mock 13 | } 14 | 15 | // GetVersion provides a mock function with no fields 16 | func (_m *Reader) GetVersion() (xcodeversion.Version, error) { 17 | ret := _m.Called() 18 | 19 | if len(ret) == 0 { 20 | panic("no return value specified for GetVersion") 21 | } 22 | 23 | var r0 xcodeversion.Version 24 | var r1 error 25 | if rf, ok := ret.Get(0).(func() (xcodeversion.Version, error)); ok { 26 | return rf() 27 | } 28 | if rf, ok := ret.Get(0).(func() xcodeversion.Version); ok { 29 | r0 = rf() 30 | } else { 31 | r0 = ret.Get(0).(xcodeversion.Version) 32 | } 33 | 34 | if rf, ok := ret.Get(1).(func() error); ok { 35 | r1 = rf() 36 | } else { 37 | r1 = ret.Error(1) 38 | } 39 | 40 | return r0, r1 41 | } 42 | 43 | // NewXcodeVersionReader creates a new instance of Reader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 44 | // The first argument is typically a *testing.T value. 45 | func NewXcodeVersionReader(t interface { 46 | mock.TestingT 47 | Cleanup(func()) 48 | }) *Reader { 49 | mock := &Reader{} 50 | mock.Mock.Test(t) 51 | 52 | t.Cleanup(func() { mock.AssertExpectations(t) }) 53 | 54 | return mock 55 | } 56 | -------------------------------------------------------------------------------- /xcodeversion/utility.go: -------------------------------------------------------------------------------- 1 | package xcodeversion 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | ) 8 | 9 | func getXcodeVersionFromXcodebuildOutput(outStr string) (Version, error) { 10 | versionRegexp := regexp.MustCompile(`(?m)^Xcode +(\d+)(\.(\d+))?.*$`) 11 | buildVersionRegexp := regexp.MustCompile(`(?m)^Build version +(\w.*)$`) 12 | 13 | xcodeVersionMatch := versionRegexp.FindStringSubmatch(outStr) 14 | if len(xcodeVersionMatch) < 4 { 15 | return Version{}, fmt.Errorf("couldn't find Xcode version in output: (%s)", outStr) 16 | } 17 | 18 | xcodebuildVersion := xcodeVersionMatch[0] 19 | majorVersionStr := xcodeVersionMatch[1] 20 | majorVersion, err := strconv.Atoi(majorVersionStr) 21 | if err != nil { 22 | return Version{}, fmt.Errorf("failed to parse xcodebuild major version (output %s): %w", outStr, err) 23 | } 24 | 25 | minorVersion := int(0) 26 | minorVersionStr := xcodeVersionMatch[3] 27 | if minorVersionStr != "" { 28 | if minorVersion, err = strconv.Atoi(minorVersionStr); err != nil { 29 | return Version{}, fmt.Errorf("failed to parse xcodebuild minor version (output %s): %w", outStr, err) 30 | } 31 | } 32 | 33 | buildVersionMatch := buildVersionRegexp.FindStringSubmatch(outStr) 34 | buildVersion := "unknown" 35 | if len(buildVersionMatch) >= 2 { 36 | buildVersion = buildVersionMatch[1] 37 | } 38 | 39 | return Version{ 40 | Version: xcodebuildVersion, 41 | BuildVersion: buildVersion, 42 | Major: int64(majorVersion), 43 | Minor: int64(minorVersion), 44 | }, nil 45 | } 46 | -------------------------------------------------------------------------------- /xcarchive/xcarchive.go: -------------------------------------------------------------------------------- 1 | package xcarchive 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/bitrise-io/go-utils/v2/log" 7 | "github.com/bitrise-io/go-utils/v2/pathutil" 8 | ) 9 | 10 | // ArchiveReader ... 11 | type ArchiveReader struct { 12 | pathChecker pathutil.PathChecker 13 | logger log.Logger 14 | } 15 | 16 | // NewArchiveReader ... 17 | func NewArchiveReader(pathChecker pathutil.PathChecker, logger log.Logger) ArchiveReader { 18 | return ArchiveReader{ 19 | pathChecker: pathChecker, 20 | logger: logger, 21 | } 22 | } 23 | 24 | // IsMacOS try to find the Contents dir under the .app/. 25 | // If its finds it the archive is macOS. If it does not the archive is iOS. 26 | func (r ArchiveReader) IsMacOS(archPath string) (bool, error) { 27 | r.logger.Debugf("Checking archive is MacOS or iOS") 28 | infoPlistPath := filepath.Join(archPath, "Info.plist") 29 | 30 | plist, err := newPlistDataFromFile(infoPlistPath) 31 | if err != nil { 32 | return false, err 33 | } 34 | 35 | appProperties, found := plist.GetMapStringInterface("ApplicationProperties") 36 | if !found { 37 | return false, err 38 | } 39 | 40 | applicationPath, found := appProperties.GetString("ApplicationPath") 41 | if !found { 42 | return false, err 43 | } 44 | 45 | applicationPath = filepath.Join(archPath, "Products", applicationPath) 46 | contentsPath := filepath.Join(applicationPath, "Contents") 47 | 48 | exist, err := r.pathChecker.IsDirExists(contentsPath) 49 | if err != nil { 50 | return false, err 51 | } 52 | 53 | return exist, nil 54 | } 55 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/appstoreconnect/jwt.go: -------------------------------------------------------------------------------- 1 | package appstoreconnect 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/x509" 6 | "encoding/pem" 7 | "errors" 8 | "fmt" 9 | "time" 10 | 11 | "github.com/golang-jwt/jwt/v4" 12 | ) 13 | 14 | // signToken signs the JWT token with the given .p8 private key content 15 | func signToken(token *jwt.Token, privateKeyContent []byte) (string, error) { 16 | block, _ := pem.Decode(privateKeyContent) 17 | if block == nil { 18 | return "", errors.New("failed to parse private key as a PEM format") 19 | } 20 | key, err := x509.ParsePKCS8PrivateKey(block.Bytes) 21 | if err != nil { 22 | return "", fmt.Errorf("failed to sign JWT token, private key format is invalid: %v", err) 23 | } 24 | 25 | privateKey, ok := key.(*ecdsa.PrivateKey) 26 | if !ok { 27 | return "", errors.New("not a private key") 28 | } 29 | 30 | return token.SignedString(privateKey) 31 | } 32 | 33 | // createToken creates a jwt.Token for the Apple API 34 | func createToken(keyID string, issuerID string, audience string) *jwt.Token { 35 | issuedAt := time.Now() 36 | expirationTime := time.Now().Add(jwtDuration) 37 | 38 | claims := jwt.RegisteredClaims{ 39 | Issuer: issuerID, 40 | IssuedAt: jwt.NewNumericDate(issuedAt), 41 | ExpiresAt: jwt.NewNumericDate(expirationTime), 42 | Audience: jwt.ClaimStrings{audience}, 43 | } 44 | 45 | // registers headers: alg = ES256 and typ = JWT 46 | token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) 47 | 48 | header := token.Header 49 | header["kid"] = keyID 50 | token.Header = header 51 | 52 | return token 53 | } 54 | -------------------------------------------------------------------------------- /logio/sink.go: -------------------------------------------------------------------------------- 1 | package logio 2 | 3 | import ( 4 | "io" 5 | "time" 6 | 7 | "github.com/globocom/go-buffer/v2" 8 | ) 9 | 10 | // Sink is an io.WriteCloser that uses a bufio.Writer to wrap the downstream and 11 | // default buffer sizes and the regular flushing of the buffer for convenience. 12 | type Sink struct { 13 | io.WriteCloser 14 | bufferedWriter bufferedWriter 15 | err chan error 16 | } 17 | 18 | // NewSink creates a new Sink instance 19 | func NewSink(downstream io.Writer) *Sink { 20 | errors := make(chan error, 10) 21 | 22 | return &Sink{ 23 | bufferedWriter: buffer.New( 24 | // Flush after five writes 25 | buffer.WithSize(5), 26 | // Flushed every second if not full 27 | buffer.WithFlushInterval(time.Second), 28 | // Flush writes to downstream 29 | buffer.WithFlusher(buffer.FlusherFunc(func(items []interface{}) { 30 | for _, item := range items { 31 | _, err := downstream.Write(item.([]byte)) 32 | 33 | select { 34 | case errors <- err: 35 | default: 36 | } 37 | } 38 | })), 39 | ), 40 | err: errors, 41 | } 42 | } 43 | 44 | // Errors is a receive only channel where the sink can communicate 45 | // errors happened on sending, should the user be interested in them 46 | func (s *Sink) Errors() <-chan error { 47 | return s.err 48 | } 49 | 50 | // Write conformance 51 | func (s *Sink) Write(p []byte) (int, error) { 52 | return len(p), s.bufferedWriter.Push(p) 53 | } 54 | 55 | // Close conformance 56 | func (s *Sink) Close() error { 57 | return s.bufferedWriter.Close() 58 | } 59 | 60 | type bufferedWriter interface { 61 | Push(item any) error 62 | Close() error 63 | } 64 | -------------------------------------------------------------------------------- /autocodesign/mock_AssetWriter.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.10.0. DO NOT EDIT. 2 | 3 | package autocodesign 4 | 5 | import ( 6 | certificateutil "github.com/bitrise-io/go-xcode/certificateutil" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // MockAssetWriter is an autogenerated mock type for the AssetWriter type 11 | type MockAssetWriter struct { 12 | mock.Mock 13 | } 14 | 15 | // InstallCertificate provides a mock function with given fields: certificate 16 | func (_m *MockAssetWriter) InstallCertificate(certificate certificateutil.CertificateInfoModel) error { 17 | ret := _m.Called(certificate) 18 | 19 | var r0 error 20 | if rf, ok := ret.Get(0).(func(certificateutil.CertificateInfoModel) error); ok { 21 | r0 = rf(certificate) 22 | } else { 23 | r0 = ret.Error(0) 24 | } 25 | 26 | return r0 27 | } 28 | 29 | // InstallProfile provides a mock function with given fields: profile 30 | func (_m *MockAssetWriter) InstallProfile(profile Profile) error { 31 | ret := _m.Called(profile) 32 | 33 | var r0 error 34 | if rf, ok := ret.Get(0).(func(Profile) error); ok { 35 | r0 = rf(profile) 36 | } else { 37 | r0 = ret.Error(0) 38 | } 39 | 40 | return r0 41 | } 42 | 43 | // Write provides a mock function with given fields: codesignAssetsByDistributionType 44 | func (_m *MockAssetWriter) Write(codesignAssetsByDistributionType map[DistributionType]AppCodesignAssets) error { 45 | ret := _m.Called(codesignAssetsByDistributionType) 46 | 47 | var r0 error 48 | if rf, ok := ret.Get(0).(func(map[DistributionType]AppCodesignAssets) error); ok { 49 | r0 = rf(codesignAssetsByDistributionType) 50 | } else { 51 | r0 = ret.Error(0) 52 | } 53 | 54 | return r0 55 | } 56 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/appstoreconnectclient/devices_test.go: -------------------------------------------------------------------------------- 1 | package appstoreconnectclient 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" 9 | "github.com/bitrise-io/go-xcode/v2/devportalservice" 10 | "github.com/stretchr/testify/mock" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | type MockDeviceClient struct { 15 | mock.Mock 16 | } 17 | 18 | func (c *MockDeviceClient) Do(req *http.Request) (*http.Response, error) { 19 | fmt.Printf("do called: %#v - %#v\n", req.Method, req.URL.Path) 20 | 21 | switch { 22 | case req.URL.Path == "/v1/devices" && req.Method == "POST": 23 | return c.RegisterDevice(req) 24 | } 25 | 26 | return nil, fmt.Errorf("invalid endpoint called: %s, method: %s", req.URL.Path, req.Method) 27 | } 28 | 29 | func (c *MockDeviceClient) RegisterDevice(req *http.Request) (*http.Response, error) { 30 | args := c.Called(req) 31 | return args.Get(0).(*http.Response), args.Error(1) 32 | } 33 | 34 | func TestDeviceClient_RegisterDevice_WhenInvaludUUID(t *testing.T) { 35 | mockClient := MockDeviceClient{} 36 | mockClient.On("RegisterDevice", mock.Anything).Return(&http.Response{}, &appstoreconnect.ErrorResponse{ 37 | Response: &http.Response{ 38 | StatusCode: http.StatusConflict, 39 | }, 40 | }) 41 | 42 | client := appstoreconnect.NewClient(&mockClient, "keyID", "issueID", []byte("privateKey"), false, appstoreconnect.NoOpAnalyticsTracker{}) 43 | deviceClient := NewDeviceClient(client) 44 | 45 | got, err := deviceClient.RegisterDevice(devportalservice.TestDevice{ 46 | DeviceID: "aadd", 47 | }) 48 | 49 | require.IsType(t, appstoreconnect.DeviceRegistrationError{}, err) 50 | require.Nil(t, got) 51 | } 52 | -------------------------------------------------------------------------------- /destination/xcode_runtime_support_test.go: -------------------------------------------------------------------------------- 1 | package destination 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bitrise-io/go-xcode/v2/xcodeversion" 7 | "github.com/hashicorp/go-version" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_isRuntimeSupportedByXcode(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | runtimePlatform string 15 | runtimeVersion *version.Version 16 | xcodeVersion xcodeversion.Version 17 | want bool 18 | }{ 19 | { 20 | name: "iOS 16 on Xcode 15", 21 | runtimePlatform: "iOS", 22 | runtimeVersion: version.Must(version.NewVersion("16.4")), 23 | xcodeVersion: xcodeversion.Version{Major: 15}, 24 | want: true, 25 | }, 26 | { 27 | name: "iOS 16 on unknown Xcode version", 28 | runtimePlatform: "iOS", 29 | runtimeVersion: version.Must(version.NewVersion("16.4")), 30 | xcodeVersion: xcodeversion.Version{Major: 3}, // unknown version 31 | want: true, 32 | }, 33 | { 34 | name: "tvOS 17 on Xcode 14", 35 | runtimePlatform: "tvOS", 36 | runtimeVersion: version.Must(version.NewVersion("17")), 37 | xcodeVersion: xcodeversion.Version{Major: 14}, 38 | want: false, 39 | }, 40 | { 41 | name: "unknown platform", 42 | runtimePlatform: "walletOS", 43 | runtimeVersion: version.Must(version.NewVersion("1")), 44 | xcodeVersion: xcodeversion.Version{Major: 15}, 45 | want: true, 46 | }, 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | got := isRuntimeSupportedByXcode(tt.runtimePlatform, tt.runtimeVersion, tt.xcodeVersion) 51 | require.Equal(t, tt.want, got) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /_integration_tests/secretaccessor.go: -------------------------------------------------------------------------------- 1 | package _integration_tests 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | secretmanager "cloud.google.com/go/secretmanager/apiv1" 9 | secretmanagerpb "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb" 10 | "github.com/bitrise-io/go-utils/log" 11 | "github.com/bitrise-io/go-utils/retry" 12 | "google.golang.org/api/option" 13 | ) 14 | 15 | // SecretAccessor ... 16 | type SecretAccessor struct { 17 | ctx context.Context 18 | client *secretmanager.Client 19 | projectID string 20 | } 21 | 22 | // NewSecretAccessor ... 23 | func NewSecretAccessor(serviceAccountJSONContent, projectID string) (*SecretAccessor, error) { 24 | ctx := context.Background() 25 | client, err := secretmanager.NewClient(ctx, option.WithCredentialsJSON([]byte(serviceAccountJSONContent))) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return &SecretAccessor{ 31 | ctx: ctx, 32 | client: client, 33 | projectID: projectID, 34 | }, nil 35 | } 36 | 37 | // GetSecret ... 38 | func (m SecretAccessor) GetSecret(key string) (string, error) { 39 | secretValue := "" 40 | if err := retry.Times(3).Wait(30 * time.Second).Try(func(attempt uint) error { 41 | if attempt > 0 { 42 | log.Warnf("%d attempt failed", attempt) 43 | } 44 | 45 | name := fmt.Sprintf("projects/%s/secrets/%s/versions/latest", m.projectID, key) 46 | req := &secretmanagerpb.AccessSecretVersionRequest{ 47 | Name: name, 48 | } 49 | result, err := m.client.AccessSecretVersion(m.ctx, req) 50 | if err != nil { 51 | log.Warnf("%s", err) 52 | return err 53 | } 54 | 55 | secretValue = string(result.Payload.Data) 56 | return nil 57 | }); err != nil { 58 | return "", err 59 | } 60 | 61 | return secretValue, nil 62 | } 63 | -------------------------------------------------------------------------------- /metaparser/metaparser.go: -------------------------------------------------------------------------------- 1 | package metaparser 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/bitrise-io/go-utils/v2/fileutil" 7 | "github.com/bitrise-io/go-utils/v2/log" 8 | "github.com/bitrise-io/go-xcode/exportoptions" 9 | ) 10 | 11 | // ArtifactMetadata ... 12 | type ArtifactMetadata struct { 13 | AppInfo Info `json:"app_info"` 14 | FileSizeBytes int64 `json:"file_size_bytes"` 15 | ProvisioningInfo ProvisionInfo `json:"provisioning_info,omitempty"` 16 | Scheme string `json:"scheme,omitempty"` 17 | } 18 | 19 | // Info ... 20 | type Info struct { 21 | AppTitle string `json:"app_title"` 22 | BundleID string `json:"bundle_id"` 23 | Version string `json:"version"` 24 | BuildNumber string `json:"build_number"` 25 | MinOSVersion string `json:"min_OS_version"` 26 | DeviceFamilyList []uint64 `json:"device_family_list"` 27 | } 28 | 29 | // ProvisionInfo ... 30 | type ProvisionInfo struct { 31 | CreationDate time.Time `json:"creation_date"` 32 | ExpireDate time.Time `json:"expire_date"` 33 | DeviceUDIDList []string `json:"device_UDID_list"` 34 | TeamName string `json:"team_name"` 35 | ProfileName string `json:"profile_name"` 36 | ProvisionsAllDevices bool `json:"provisions_all_devices"` 37 | IPAExportMethod exportoptions.Method `json:"ipa_export_method"` 38 | } 39 | 40 | // Parser ... 41 | type Parser struct { 42 | logger log.Logger 43 | fileManager fileutil.FileManager 44 | } 45 | 46 | // New ... 47 | func New(logger log.Logger, fileManager fileutil.FileManager) *Parser { 48 | return &Parser{ 49 | logger: logger, 50 | fileManager: fileManager, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /errorfinder/nserror.go: -------------------------------------------------------------------------------- 1 | package errorfinder 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | type nsError struct { 9 | Description string 10 | Suggestion string 11 | } 12 | 13 | func newNSError(str string) *nsError { 14 | if !isNSError(str) { 15 | return nil 16 | } 17 | 18 | descriptionPattern := `NSLocalizedDescription=(.+?),|NSLocalizedDescription=(.+?)}` 19 | description := findFirstSubMatch(str, descriptionPattern) 20 | if description == "" { 21 | return nil 22 | } 23 | 24 | suggestionPattern := `NSLocalizedRecoverySuggestion=(.+?),|NSLocalizedRecoverySuggestion=(.+?)}` 25 | suggestion := findFirstSubMatch(str, suggestionPattern) 26 | 27 | return &nsError{ 28 | Description: description, 29 | Suggestion: suggestion, 30 | } 31 | } 32 | 33 | func (e nsError) Error() string { 34 | msg := e.Description 35 | if e.Suggestion != "" { 36 | msg += " " + e.Suggestion 37 | } 38 | return msg 39 | } 40 | 41 | func findFirstSubMatch(str, pattern string) string { 42 | exp := regexp.MustCompile(pattern) 43 | matches := exp.FindStringSubmatch(str) 44 | if len(matches) > 1 { 45 | for _, match := range matches[1:] { 46 | if match != "" { 47 | return match 48 | } 49 | } 50 | } 51 | return "" 52 | } 53 | 54 | func isNSError(str string) bool { 55 | // example: Error Domain=IDEProvisioningErrorDomain Code=9 ""ios-simple-objc.app" requires a provisioning profile." 56 | // UserInfo={IDEDistributionIssueSeverity=3, NSLocalizedDescription="ios-simple-objc.app" requires a provisioning profile., 57 | // NSLocalizedRecoverySuggestion=Add a profile to the "provisioningProfiles" dictionary in your Export Options property list.} 58 | return strings.Contains(str, "Error ") && 59 | strings.Contains(str, "Domain=") && 60 | strings.Contains(str, "Code=") && 61 | strings.Contains(str, "UserInfo=") 62 | } 63 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/time/time.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | // Time ... 10 | type Time time.Time 11 | 12 | // UnmarshalJSON ... 13 | func (t *Time) UnmarshalJSON(b []byte) error { 14 | timeStr := strings.Trim(string(b), `"`) 15 | var errors []error 16 | 17 | for _, timeFormat := range timeFormats() { 18 | parsed, err := time.Parse(timeFormat, timeStr) 19 | if err != nil { 20 | errors = append(errors, err) 21 | continue 22 | } 23 | 24 | *t = Time(parsed) 25 | return nil 26 | } 27 | 28 | return fmt.Errorf("%s", errors) 29 | } 30 | 31 | func timeFormats() []string { 32 | formats := []string{time.RFC3339} 33 | formats = append(formats, appleKeyAuthTimeFormats()...) 34 | formats = append(formats, appleIDAuthTimeFormats()...) 35 | 36 | return formats 37 | } 38 | 39 | func appleKeyAuthTimeFormats() []string { 40 | // Apple is using an ISO 8601 time format (https://en.wikipedia.org/wiki/ISO_8601). In this format the offset from 41 | // the UTC time can have the following equivalent and interchangeable formats: 42 | // * [+/-]07:00 43 | // * [+/-]0700 44 | // * [+/-]07 45 | // (* also if there is no UTC offset then [+0000, +00:00, +00] are the same as adding a Z after the seconds) 46 | // 47 | // Go has built in support for ISO 8601 but only for the zero offset UTC and the [+/-]07:00 format under time.RFC3339. 48 | // We still need to check for the other two. 49 | return []string{ 50 | "2006-01-02T15:04:05.000-0700", 51 | "2006-01-02T15:04:05.000-07", 52 | } 53 | } 54 | 55 | func appleIDAuthTimeFormats() []string { 56 | // Spaceship returns this time format when setting SPACESHIP_AVOID_XCODE_API=true. This is needed because Apple's 57 | // API started to return an error for the old spaceship implementation. 58 | return []string{ 59 | "2006-01-02 15:04:05 UTC", 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/zip/stdlib_reader.go: -------------------------------------------------------------------------------- 1 | package zip 2 | 3 | import ( 4 | "archive/zip" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "sort" 9 | 10 | "github.com/bitrise-io/go-utils/v2/log" 11 | "github.com/ryanuber/go-glob" 12 | ) 13 | 14 | // StdlibRead ... 15 | type StdlibRead struct { 16 | zipReader *zip.ReadCloser 17 | logger log.Logger 18 | } 19 | 20 | // NewStdlibRead ... 21 | func NewStdlibRead(archivePath string, logger log.Logger) (*StdlibRead, error) { 22 | zipReader, err := zip.OpenReader(archivePath) 23 | if err != nil { 24 | return nil, fmt.Errorf("failed to open archive %s: %w", archivePath, err) 25 | } 26 | 27 | return &StdlibRead{ 28 | zipReader: zipReader, 29 | logger: logger, 30 | }, nil 31 | } 32 | 33 | // ReadFile ... 34 | func (r StdlibRead) ReadFile(relPthPattern string) ([]byte, error) { 35 | var files []*zip.File 36 | for _, f := range r.zipReader.File { 37 | if glob.Glob(relPthPattern, f.Name) { 38 | files = append(files, f) 39 | } 40 | } 41 | 42 | if len(files) == 0 { 43 | return nil, fmt.Errorf("no file found with pattern: %s", relPthPattern) 44 | } 45 | 46 | sort.Slice(files, func(i, j int) bool { 47 | return len(files[i].Name) < len(files[j].Name) 48 | }) 49 | 50 | file := files[0] 51 | f, err := file.Open() 52 | if err != nil { 53 | return nil, fmt.Errorf("failed to open %s: %w", file.Name, err) 54 | } 55 | defer func() { 56 | if err := f.Close(); err != nil { 57 | r.logger.Warnf("Failed to close %s: %s", file.Name, err) 58 | } 59 | }() 60 | 61 | b, err := io.ReadAll(f) 62 | if err != nil { 63 | return nil, fmt.Errorf("failed to read %s: %w", file.Name, err) 64 | } 65 | 66 | return b, nil 67 | } 68 | 69 | // Close ... 70 | func (r StdlibRead) Close() error { 71 | return r.zipReader.Close() 72 | } 73 | 74 | // IsErrFormat ... 75 | func IsErrFormat(err error) bool { 76 | return errors.Is(err, zip.ErrFormat) 77 | } 78 | -------------------------------------------------------------------------------- /xcodecommand/xcodebuild_only.go: -------------------------------------------------------------------------------- 1 | package xcodecommand 2 | 3 | import ( 4 | "bytes" 5 | "time" 6 | 7 | "github.com/bitrise-io/go-utils/progress" 8 | "github.com/bitrise-io/go-utils/v2/command" 9 | "github.com/bitrise-io/go-utils/v2/log" 10 | "github.com/bitrise-io/go-xcode/v2/errorfinder" 11 | version "github.com/hashicorp/go-version" 12 | ) 13 | 14 | var unbufferedIOEnv = []string{"NSUnbufferedIO=YES"} 15 | 16 | // RawXcodeCommandRunner is an xcodebuild runner that uses no additional log formatter 17 | type RawXcodeCommandRunner struct { 18 | logger log.Logger 19 | commandFactory command.Factory 20 | } 21 | 22 | // NewRawCommandRunner creates a new RawXcodeCommandRunner 23 | func NewRawCommandRunner(logger log.Logger, commandFactory command.Factory) Runner { 24 | return &RawXcodeCommandRunner{ 25 | logger: logger, 26 | commandFactory: commandFactory, 27 | } 28 | } 29 | 30 | // Run runs xcodebuild using no additional log formatter 31 | func (c *RawXcodeCommandRunner) Run(workDir string, args []string, _ []string) (Output, error) { 32 | var ( 33 | outBuffer bytes.Buffer 34 | err error 35 | exitCode int 36 | ) 37 | 38 | command := c.commandFactory.Create("xcodebuild", args, &command.Opts{ 39 | Stdout: &outBuffer, 40 | Stderr: &outBuffer, 41 | Env: unbufferedIOEnv, 42 | Dir: workDir, 43 | ErrorFinder: errorfinder.FindXcodebuildErrors, 44 | }) 45 | 46 | c.logger.TPrintf("$ %s", command.PrintableCommandArgs()) 47 | 48 | progress.SimpleProgress(".", time.Minute, func() { 49 | exitCode, err = command.RunAndReturnExitCode() 50 | }) 51 | 52 | return Output{ 53 | RawOut: outBuffer.Bytes(), 54 | ExitCode: exitCode, 55 | }, err 56 | } 57 | 58 | // CheckInstall does nothing as no additional log formatter is used 59 | func (c *RawXcodeCommandRunner) CheckInstall() (*version.Version, error) { 60 | return nil, nil 61 | } 62 | -------------------------------------------------------------------------------- /mocks/PathChecker.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // PathChecker is an autogenerated mock type for the PathChecker type 8 | type PathChecker struct { 9 | mock.Mock 10 | } 11 | 12 | // IsDirExists provides a mock function with given fields: pth 13 | func (_m *PathChecker) IsDirExists(pth string) (bool, error) { 14 | ret := _m.Called(pth) 15 | 16 | var r0 bool 17 | var r1 error 18 | if rf, ok := ret.Get(0).(func(string) (bool, error)); ok { 19 | return rf(pth) 20 | } 21 | if rf, ok := ret.Get(0).(func(string) bool); ok { 22 | r0 = rf(pth) 23 | } else { 24 | r0 = ret.Get(0).(bool) 25 | } 26 | 27 | if rf, ok := ret.Get(1).(func(string) error); ok { 28 | r1 = rf(pth) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | 36 | // IsPathExists provides a mock function with given fields: pth 37 | func (_m *PathChecker) IsPathExists(pth string) (bool, error) { 38 | ret := _m.Called(pth) 39 | 40 | var r0 bool 41 | var r1 error 42 | if rf, ok := ret.Get(0).(func(string) (bool, error)); ok { 43 | return rf(pth) 44 | } 45 | if rf, ok := ret.Get(0).(func(string) bool); ok { 46 | r0 = rf(pth) 47 | } else { 48 | r0 = ret.Get(0).(bool) 49 | } 50 | 51 | if rf, ok := ret.Get(1).(func(string) error); ok { 52 | r1 = rf(pth) 53 | } else { 54 | r1 = ret.Error(1) 55 | } 56 | 57 | return r0, r1 58 | } 59 | 60 | // NewPathChecker creates a new instance of PathChecker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 61 | // The first argument is typically a *testing.T value. 62 | func NewPathChecker(t interface { 63 | mock.TestingT 64 | Cleanup(func()) 65 | }) *PathChecker { 66 | mock := &PathChecker{} 67 | mock.Mock.Test(t) 68 | 69 | t.Cleanup(func() { mock.AssertExpectations(t) }) 70 | 71 | return mock 72 | } 73 | -------------------------------------------------------------------------------- /xcodecache/derived_data_path.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/binary" 6 | "fmt" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/bitrise-io/go-utils/pathutil" 11 | "golang.org/x/text/unicode/norm" 12 | ) 13 | 14 | // xcodeDerivedDataHash returns the unique ID generated by Xcode for a xcodeproj or xcworkspace path, used in DerivedData build directory name. 15 | func xcodeDerivedDataHash(path string) (string, error) { 16 | // Suppress insecure crypto primitive warning: #nosec G401 17 | hasher := md5.New() 18 | if _, err := hasher.Write([]byte(norm.NFD.String(path))); err != nil { 19 | return "", err 20 | } 21 | result := make([]byte, 28) 22 | 23 | // take the first 8 bytes of the hash with swapped byte order 24 | startValue := binary.BigEndian.Uint64(hasher.Sum(nil)) 25 | for i := 13; i >= 0; i-- { 26 | // mod 26 (restricting to alphabetic) and add to 'a' 27 | result[i] = byte(startValue%26) + 'a' 28 | startValue /= 26 29 | } 30 | 31 | // Same operation on the last 8 bytes 32 | startValue = binary.BigEndian.Uint64(hasher.Sum(nil)[8:]) 33 | for i := 27; i > 13; i-- { 34 | result[i] = byte(startValue%26) + 'a' 35 | startValue /= 26 36 | } 37 | 38 | return string(result), nil 39 | } 40 | 41 | // xcodeProjectDerivedDataPath return a per project or worksapce Xcode DerivedData path. 42 | func xcodeProjectDerivedDataPath(projectPath string) (string, error) { 43 | projectName := strings.TrimSuffix(filepath.Base(projectPath), filepath.Ext(projectPath)) 44 | projectName = strings.Replace(projectName, " ", "_", -1) // changing spaces to _ 45 | derivedDataDir := filepath.Join(pathutil.UserHomeDir(), "Library", "Developer", "Xcode", "DerivedData") 46 | 47 | projectHash, err := xcodeDerivedDataHash(projectPath) 48 | if err != nil { 49 | return "", fmt.Errorf("failed to get per-project DerivedData path hash, %s", err) 50 | } 51 | 52 | return filepath.Join(derivedDataDir, fmt.Sprintf("%s-%s", projectName, projectHash)), nil 53 | } 54 | -------------------------------------------------------------------------------- /codesign/mocks/DetailsProvider.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.9.4. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | autocodesign "github.com/bitrise-io/go-xcode/v2/autocodesign" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // DetailsProvider is an autogenerated mock type for the DetailsProvider type 12 | type DetailsProvider struct { 13 | mock.Mock 14 | } 15 | 16 | // GetAppLayout provides a mock function with given fields: uiTestTargets 17 | func (_m *DetailsProvider) GetAppLayout(uiTestTargets bool) (autocodesign.AppLayout, error) { 18 | ret := _m.Called(uiTestTargets) 19 | 20 | var r0 autocodesign.AppLayout 21 | if rf, ok := ret.Get(0).(func(bool) autocodesign.AppLayout); ok { 22 | r0 = rf(uiTestTargets) 23 | } else { 24 | r0 = ret.Get(0).(autocodesign.AppLayout) 25 | } 26 | 27 | var r1 error 28 | if rf, ok := ret.Get(1).(func(bool) error); ok { 29 | r1 = rf(uiTestTargets) 30 | } else { 31 | r1 = ret.Error(1) 32 | } 33 | 34 | return r0, r1 35 | } 36 | 37 | // IsSigningManagedAutomatically provides a mock function with given fields: 38 | func (_m *DetailsProvider) IsSigningManagedAutomatically() (bool, error) { 39 | ret := _m.Called() 40 | 41 | var r0 bool 42 | if rf, ok := ret.Get(0).(func() bool); ok { 43 | r0 = rf() 44 | } else { 45 | r0 = ret.Get(0).(bool) 46 | } 47 | 48 | var r1 error 49 | if rf, ok := ret.Get(1).(func() error); ok { 50 | r1 = rf() 51 | } else { 52 | r1 = ret.Error(1) 53 | } 54 | 55 | return r0, r1 56 | } 57 | 58 | // Platform provides a mock function with given fields: 59 | func (_m *DetailsProvider) Platform() (autocodesign.Platform, error) { 60 | ret := _m.Called() 61 | 62 | var r0 autocodesign.Platform 63 | if rf, ok := ret.Get(0).(func() autocodesign.Platform); ok { 64 | r0 = rf() 65 | } else { 66 | r0 = ret.Get(0).(autocodesign.Platform) 67 | } 68 | 69 | var r1 error 70 | if rf, ok := ret.Get(1).(func() error); ok { 71 | r1 = rf() 72 | } else { 73 | r1 = ret.Error(1) 74 | } 75 | 76 | return r0, r1 77 | } 78 | -------------------------------------------------------------------------------- /_integration_tests/test_helper.go: -------------------------------------------------------------------------------- 1 | package _integration_tests 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/bitrise-io/go-utils/command/git" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | const sampleArtifactsRepoURL = "https://github.com/bitrise-io/sample-artifacts.git" 13 | 14 | var reposToDir map[string]map[string]string 15 | 16 | func GetSampleArtifactsRepository(t require.TestingT) string { 17 | return GetRepository(t, sampleArtifactsRepoURL, "master") 18 | } 19 | 20 | func GetRepository(t require.TestingT, url, branch string) string { 21 | if repoDir := getRepoDir(url, branch); repoDir != "" { 22 | return repoDir 23 | } 24 | 25 | tmpDir := createDirForRepo(t, url, branch) 26 | gitCommand, err := git.New(tmpDir) 27 | require.NoError(t, err) 28 | 29 | out, err := gitCommand.Clone(url, "--depth=1", "--branch", branch).RunAndReturnTrimmedCombinedOutput() 30 | require.NoError(t, err, out) 31 | 32 | saveRepoDir(tmpDir, url, branch) 33 | 34 | return tmpDir 35 | } 36 | 37 | func getRepoDir(url, branch string) string { 38 | if reposToDir == nil { 39 | return "" 40 | } 41 | 42 | branchToDir, ok := reposToDir[url] 43 | if !ok { 44 | return "" 45 | } 46 | 47 | dir, ok := branchToDir[branch] 48 | if !ok { 49 | return "" 50 | } 51 | return dir 52 | } 53 | 54 | func saveRepoDir(dir, url, branch string) { 55 | if reposToDir == nil { 56 | reposToDir = map[string]map[string]string{} 57 | } 58 | 59 | branchToDir, ok := reposToDir[url] 60 | if !ok { 61 | branchToDir = map[string]string{} 62 | } 63 | 64 | branchToDir[branch] = dir 65 | reposToDir[url] = branchToDir 66 | } 67 | 68 | func createDirForRepo(t require.TestingT, repo, branch string) string { 69 | tmpDir, err := os.MkdirTemp("", "go-xcode") 70 | require.NoError(t, err) 71 | 72 | repoRootDir := strings.TrimSuffix(filepath.Base(repo), filepath.Ext(repo)) 73 | pth := filepath.Join(tmpDir, repoRootDir, branch) 74 | err = os.MkdirAll(pth, os.ModePerm) 75 | require.NoError(t, err) 76 | 77 | return pth 78 | } 79 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/spaceship/portal/certificate_client.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship' 2 | 3 | require_relative 'common' 4 | 5 | module Portal 6 | # CertificateClient ... 7 | class CertificateClient 8 | def self.download_development_certificates 9 | development_certificates = [] 10 | run_or_raise_preferred_error_message do 11 | development_certificates = Spaceship::Portal.certificate.development.all 12 | development_certificates.concat(Spaceship::Portal.certificate.apple_development.all) 13 | end 14 | 15 | certificates = [] 16 | development_certificates.each do |cert| 17 | if cert.can_download 18 | certificates.push(cert) 19 | else 20 | Log.debug("development certificate: #{cert.name} is not downloadable, skipping...") 21 | end 22 | end 23 | 24 | certificates 25 | end 26 | 27 | def self.download_production_certificates 28 | production_certificates = [] 29 | run_or_raise_preferred_error_message do 30 | production_certificates = Spaceship::Portal.certificate.production.all 31 | production_certificates.concat(Spaceship::Portal.certificate.apple_distribution.all) 32 | end 33 | 34 | certificates = [] 35 | production_certificates.each do |cert| 36 | if cert.can_download 37 | certificates.push(cert) 38 | else 39 | Log.debug("production certificate: #{cert.name} is not downloadable, skipping...") 40 | end 41 | end 42 | 43 | if production_certificates.to_a.empty? 44 | run_or_raise_preferred_error_message { production_certificates = Spaceship::Portal.certificate.in_house.all } 45 | 46 | production_certificates.each do |cert| 47 | if cert.can_download 48 | certificates.push(cert) 49 | else 50 | Log.debug("production certificate: #{cert.name} is not downloadable, skipping...") 51 | end 52 | end 53 | end 54 | 55 | certificates 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /autocodesign/mock_LocalCodeSignAssetManager.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery 2.9.4. DO NOT EDIT. 2 | 3 | package autocodesign 4 | 5 | import ( 6 | appstoreconnect "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // MockLocalCodeSignAssetManager is an autogenerated mock type for the LocalCodeSignAssetManager type 11 | type MockLocalCodeSignAssetManager struct { 12 | mock.Mock 13 | } 14 | 15 | // FindCodesignAssets provides a mock function with given fields: appLayout, distrType, certsByType, deviceIDs, minProfileDaysValid 16 | func (_m *MockLocalCodeSignAssetManager) FindCodesignAssets(appLayout AppLayout, distrType DistributionType, certsByType map[appstoreconnect.CertificateType][]Certificate, deviceIDs []string, minProfileDaysValid int) (*AppCodesignAssets, *AppLayout, error) { 17 | ret := _m.Called(appLayout, distrType, certsByType, deviceIDs, minProfileDaysValid) 18 | 19 | var r0 *AppCodesignAssets 20 | if rf, ok := ret.Get(0).(func(AppLayout, DistributionType, map[appstoreconnect.CertificateType][]Certificate, []string, int) *AppCodesignAssets); ok { 21 | r0 = rf(appLayout, distrType, certsByType, deviceIDs, minProfileDaysValid) 22 | } else { 23 | if ret.Get(0) != nil { 24 | r0, ok = ret.Get(0).(*AppCodesignAssets) 25 | if !ok { 26 | } 27 | } 28 | } 29 | 30 | var r1 *AppLayout 31 | if rf, ok := ret.Get(1).(func(AppLayout, DistributionType, map[appstoreconnect.CertificateType][]Certificate, []string, int) *AppLayout); ok { 32 | r1 = rf(appLayout, distrType, certsByType, deviceIDs, minProfileDaysValid) 33 | } else { 34 | if ret.Get(1) != nil { 35 | r1, ok = ret.Get(1).(*AppLayout) 36 | if !ok { 37 | } 38 | } 39 | } 40 | 41 | var r2 error 42 | if rf, ok := ret.Get(2).(func(AppLayout, DistributionType, map[appstoreconnect.CertificateType][]Certificate, []string, int) error); ok { 43 | r2 = rf(appLayout, distrType, certsByType, deviceIDs, minProfileDaysValid) 44 | } else { 45 | r2 = ret.Error(2) 46 | } 47 | 48 | return r0, r1, r2 49 | } 50 | -------------------------------------------------------------------------------- /destination/destination.go: -------------------------------------------------------------------------------- 1 | package destination 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | genericPlatformKey = "generic/platform" 10 | platformKey = "platform" 11 | nameKey = "name" 12 | osKey = "OS" 13 | archKey = "arch" 14 | ) 15 | 16 | // Platform ... 17 | type Platform string 18 | 19 | // Platforms ... 20 | const ( 21 | MacOS Platform = "macOS" 22 | IOS Platform = "iOS" 23 | IOSSimulator Platform = "iOS Simulator" 24 | WatchOS Platform = "watchOS" 25 | WatchOSSimulator Platform = "watchOS Simulator" 26 | TvOS Platform = "tvOS" 27 | TvOSSimulator Platform = "tvOS Simulator" 28 | DriverKit Platform = "DriverKit" 29 | VisionOS Platform = "visionOS" 30 | VisionOSSimulator Platform = "visionOS Simulator" 31 | ) 32 | 33 | // Specifier ... 34 | type Specifier map[string]string 35 | 36 | // NewSpecifier ... 37 | func NewSpecifier(destination string) (Specifier, error) { 38 | specifier := Specifier{} 39 | 40 | parts := strings.Split(destination, ",") 41 | for _, part := range parts { 42 | keyAndValue := strings.Split(part, "=") 43 | 44 | if len(keyAndValue) != 2 { 45 | return nil, fmt.Errorf(`could not parse "%s" because it is not a valid key=value pair in destination: %s`, part, destination) 46 | } 47 | 48 | key := keyAndValue[0] 49 | value := keyAndValue[1] 50 | 51 | specifier[key] = value 52 | } 53 | 54 | return specifier, nil 55 | } 56 | 57 | // Platform returns the platform part of the specifier and true if it's the generic platform 58 | func (s Specifier) Platform() (Platform, bool) { 59 | p, ok := s[genericPlatformKey] 60 | if ok { 61 | return Platform(p), true 62 | } 63 | 64 | return Platform(s[platformKey]), false 65 | } 66 | 67 | // Name ... 68 | func (s Specifier) Name() string { 69 | return s[nameKey] 70 | } 71 | 72 | // OS ... 73 | func (s Specifier) OS() string { 74 | return s[osKey] 75 | } 76 | 77 | // Arch ... 78 | func (s Specifier) Arch() string { 79 | return s[archKey] 80 | } 81 | -------------------------------------------------------------------------------- /autocodesign/models.go: -------------------------------------------------------------------------------- 1 | package autocodesign 2 | 3 | import ( 4 | "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" 5 | ) 6 | 7 | // CertificateTypeByDistribution ... 8 | var CertificateTypeByDistribution = map[DistributionType]appstoreconnect.CertificateType{ 9 | Development: appstoreconnect.IOSDevelopment, 10 | AppStore: appstoreconnect.IOSDistribution, 11 | AdHoc: appstoreconnect.IOSDistribution, 12 | Enterprise: appstoreconnect.IOSDistribution, 13 | } 14 | 15 | // ProfileTypeToPlatform ... 16 | var ProfileTypeToPlatform = map[appstoreconnect.ProfileType]Platform{ 17 | appstoreconnect.IOSAppDevelopment: IOS, 18 | appstoreconnect.IOSAppStore: IOS, 19 | appstoreconnect.IOSAppAdHoc: IOS, 20 | appstoreconnect.IOSAppInHouse: IOS, 21 | 22 | appstoreconnect.TvOSAppDevelopment: TVOS, 23 | appstoreconnect.TvOSAppStore: TVOS, 24 | appstoreconnect.TvOSAppAdHoc: TVOS, 25 | appstoreconnect.TvOSAppInHouse: TVOS, 26 | } 27 | 28 | // ProfileTypeToDistribution ... 29 | var ProfileTypeToDistribution = map[appstoreconnect.ProfileType]DistributionType{ 30 | appstoreconnect.IOSAppDevelopment: Development, 31 | appstoreconnect.IOSAppStore: AppStore, 32 | appstoreconnect.IOSAppAdHoc: AdHoc, 33 | appstoreconnect.IOSAppInHouse: Enterprise, 34 | 35 | appstoreconnect.TvOSAppDevelopment: Development, 36 | appstoreconnect.TvOSAppStore: AppStore, 37 | appstoreconnect.TvOSAppAdHoc: AdHoc, 38 | appstoreconnect.TvOSAppInHouse: Enterprise, 39 | } 40 | 41 | // PlatformToProfileTypeByDistribution ... 42 | var PlatformToProfileTypeByDistribution = map[Platform]map[DistributionType]appstoreconnect.ProfileType{ 43 | IOS: { 44 | Development: appstoreconnect.IOSAppDevelopment, 45 | AppStore: appstoreconnect.IOSAppStore, 46 | AdHoc: appstoreconnect.IOSAppAdHoc, 47 | Enterprise: appstoreconnect.IOSAppInHouse, 48 | }, 49 | TVOS: { 50 | Development: appstoreconnect.TvOSAppDevelopment, 51 | AppStore: appstoreconnect.TvOSAppStore, 52 | AdHoc: appstoreconnect.TvOSAppAdHoc, 53 | Enterprise: appstoreconnect.TvOSAppInHouse, 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/spaceship_test.go: -------------------------------------------------------------------------------- 1 | package spaceship 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/bitrise-io/go-xcode/v2/mocks" 8 | "github.com/stretchr/testify/mock" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | const spaceshipTemporaryUnavailableOutput = `Apple ID authentication failed: 13 | 503 Service Temporarily Unavailable 14 | 15 |

503 Service Temporarily Unavailable

16 |
Apple
17 | 18 | ` 19 | 20 | func Test_runSpaceshipCommand_retries_on_temporarily_unavailable_error(t *testing.T) { 21 | cmd := new(mocks.Command) 22 | cmd.On("RunAndReturnTrimmedCombinedOutput").Return(spaceshipTemporaryUnavailableOutput, errors.New("exit status 1")).Once() 23 | cmd.On("RunAndReturnTrimmedCombinedOutput").Return(spaceshipTemporaryUnavailableOutput, errors.New("exit status 1")).Once() 24 | cmd.On("RunAndReturnTrimmedCombinedOutput").Return("{}", nil).Once() 25 | 26 | cmdFactory := new(mocks.RubyCommandFactory) 27 | cmdFactory.On("CreateBundleExec", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(cmd) 28 | 29 | c := &Client{ 30 | cmdFactory: cmdFactory, 31 | isNoSleepRetry: true, 32 | } 33 | out, err := c.runSpaceshipCommand("") 34 | require.NoError(t, err) 35 | require.Equal(t, "{}", out) 36 | 37 | cmd.AssertExpectations(t) 38 | cmdFactory.AssertExpectations(t) 39 | } 40 | 41 | func Test_runSpaceshipCommand_retries_only_temporarily_unavailable_error(t *testing.T) { 42 | cmd := new(mocks.Command) 43 | cmd.On("RunAndReturnTrimmedCombinedOutput").Return("exit status 1", errors.New("exit status 1")).Once() 44 | 45 | cmdFactory := new(mocks.RubyCommandFactory) 46 | cmdFactory.On("CreateBundleExec", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(cmd) 47 | 48 | c := &Client{ 49 | cmdFactory: cmdFactory, 50 | } 51 | out, err := c.runSpaceshipCommand("") 52 | require.EqualError(t, err, "spaceship command failed with output: exit status 1") 53 | require.Equal(t, "", out) 54 | 55 | cmd.AssertExpectations(t) 56 | cmdFactory.AssertExpectations(t) 57 | } 58 | -------------------------------------------------------------------------------- /xcodecommand/xcpretty_installer.go: -------------------------------------------------------------------------------- 1 | package xcodecommand 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bitrise-io/go-steputils/v2/ruby" 7 | command "github.com/bitrise-io/go-utils/v2/command" 8 | version "github.com/hashicorp/go-version" 9 | ) 10 | 11 | type xcprettyManager interface { 12 | isDepInstalled() (bool, error) 13 | installDep() []command.Command 14 | depVersion() (*version.Version, error) 15 | } 16 | 17 | type xcpretty struct { 18 | commandFactory command.Factory 19 | rubyEnv ruby.Environment 20 | rubyCommandFactory ruby.CommandFactory 21 | } 22 | 23 | // CheckInstall checks if xcpretty is isntalled, if not installs it. 24 | // Returns its version. 25 | func (c *XcprettyCommandRunner) CheckInstall() (*version.Version, error) { 26 | c.logger.Println() 27 | c.logger.Infof("Checking if log formatter (xcpretty) is installed") 28 | 29 | installed, err := c.xcpretty.isDepInstalled() 30 | if err != nil { 31 | return nil, err 32 | } else if !installed { 33 | c.logger.Warnf(`xcpretty is not installed`) 34 | fmt.Println() 35 | c.logger.Printf("Installing xcpretty") 36 | 37 | cmdModelSlice := c.xcpretty.installDep() 38 | for _, cmd := range cmdModelSlice { 39 | if err := cmd.Run(); err != nil { 40 | return nil, fmt.Errorf("failed to run xcpretty install command (%s): %w", cmd.PrintableCommandArgs(), err) 41 | } 42 | } 43 | } 44 | 45 | xcprettyVersion, err := c.xcpretty.depVersion() 46 | if err != nil { 47 | return nil, fmt.Errorf("failed to get xcpretty version: %w", err) 48 | } 49 | 50 | return xcprettyVersion, nil 51 | } 52 | 53 | func (c *xcpretty) isDepInstalled() (bool, error) { 54 | return c.rubyEnv.IsGemInstalled("xcpretty", "") 55 | } 56 | 57 | func (c *xcpretty) installDep() []command.Command { 58 | cmds := c.rubyCommandFactory.CreateGemInstall("xcpretty", "", false, false, nil) 59 | return cmds 60 | } 61 | 62 | func (c *xcpretty) depVersion() (*version.Version, error) { 63 | cmd := c.commandFactory.Create("xcpretty", []string{"--version"}, nil) 64 | 65 | versionOut, err := cmd.RunAndReturnTrimmedCombinedOutput() 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return version.NewVersion(versionOut) 71 | } 72 | -------------------------------------------------------------------------------- /autocodesign/profiledownloader/profiledownloader.go: -------------------------------------------------------------------------------- 1 | package profiledownloader 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/bitrise-io/go-steputils/input" 10 | "github.com/bitrise-io/go-utils/filedownloader" 11 | "github.com/bitrise-io/go-xcode/profileutil" 12 | "github.com/bitrise-io/go-xcode/v2/autocodesign" 13 | "github.com/bitrise-io/go-xcode/v2/autocodesign/localcodesignasset" 14 | ) 15 | 16 | type downloader struct { 17 | urls []string 18 | client *http.Client 19 | } 20 | 21 | // New returns an implementation that can download from remote, local file paths 22 | func New(profileURLs []string, client *http.Client) autocodesign.ProfileProvider { 23 | return downloader{ 24 | urls: profileURLs, 25 | client: client, 26 | } 27 | } 28 | 29 | // IsAvailable returns true if there are available remote profiles to download 30 | func (d downloader) IsAvailable() bool { 31 | return len(d.urls) != 0 32 | } 33 | 34 | // GetProfiles downloads remote profiles and returns their contents 35 | func (d downloader) GetProfiles() ([]autocodesign.LocalProfile, error) { 36 | var profiles []autocodesign.LocalProfile 37 | 38 | for _, url := range d.urls { 39 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) 40 | defer cancel() 41 | 42 | downloader := filedownloader.NewWithContext(ctx, d.client) 43 | fileProvider := input.NewFileProvider(downloader) 44 | 45 | content, err := fileProvider.Contents(url) 46 | if err != nil { 47 | return nil, err 48 | } else if content == nil { 49 | return nil, fmt.Errorf("profile (%s) is empty", url) 50 | } 51 | 52 | parsedProfile, err := profileutil.ProvisioningProfileFromContent(content) 53 | if err != nil { 54 | return nil, fmt.Errorf("invalid pkcs7 file format: %w", err) 55 | } 56 | 57 | profileInfo, err := profileutil.NewProvisioningProfileInfo(*parsedProfile) 58 | if err != nil { 59 | return nil, fmt.Errorf("unknown provisioning profile format: %w", err) 60 | } 61 | 62 | profiles = append(profiles, autocodesign.LocalProfile{ 63 | Profile: localcodesignasset.NewProfile(profileInfo, content), 64 | Info: profileInfo, 65 | }) 66 | } 67 | 68 | return profiles, nil 69 | } 70 | -------------------------------------------------------------------------------- /xcconfig/xcconfig.go: -------------------------------------------------------------------------------- 1 | package xcconfig 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/bitrise-io/go-utils/v2/fileutil" 9 | "github.com/bitrise-io/go-utils/v2/pathutil" 10 | ) 11 | 12 | // Writer ... 13 | type Writer interface { 14 | Write(input string) (string, error) 15 | } 16 | 17 | type writer struct { 18 | pathProvider pathutil.PathProvider 19 | fileManager fileutil.FileManager 20 | pathChecker pathutil.PathChecker 21 | pathModifier pathutil.PathModifier 22 | } 23 | 24 | // NewWriter ... 25 | func NewWriter(pathProvider pathutil.PathProvider, fileManager fileutil.FileManager, pathChecker pathutil.PathChecker, pathModifier pathutil.PathModifier) Writer { 26 | return &writer{pathProvider: pathProvider, fileManager: fileManager, pathChecker: pathChecker, pathModifier: pathModifier} 27 | } 28 | 29 | // Write writes the contents of input into a xcconfig file if 30 | // the provided content is not already a path to xcconfig file. 31 | // If the content is a valid path to xcconfig, it will validate the path, 32 | // and return the path. It returns error if it cannot finalize a xcconfig 33 | // file and/or its path. 34 | func (w writer) Write(input string) (string, error) { 35 | if w.isPath(input) { 36 | xcconfigPath, err := w.pathModifier.AbsPath(input) 37 | if err != nil { 38 | return "", fmt.Errorf("failed to convert xcconfig file path (%s) to absolute path: %w", input, err) 39 | } 40 | 41 | pathExists, err := w.pathChecker.IsPathExists(xcconfigPath) 42 | if err != nil { 43 | return "", err 44 | } 45 | if !pathExists { 46 | return "", fmt.Errorf("provided xcconfig file path doesn't exist: %s", input) 47 | } 48 | return xcconfigPath, nil 49 | } 50 | 51 | dir, err := w.pathProvider.CreateTempDir("") 52 | if err != nil { 53 | return "", fmt.Errorf("unable to create temp dir for writing XCConfig: %v", err) 54 | } 55 | xcconfigPath := filepath.Join(dir, "temp.xcconfig") 56 | if err = w.fileManager.Write(xcconfigPath, input, 0644); err != nil { 57 | return "", fmt.Errorf("unable to write XCConfig content into file: %v", err) 58 | } 59 | return xcconfigPath, nil 60 | } 61 | 62 | func (w writer) isPath(input string) bool { 63 | return strings.HasSuffix(input, ".xcconfig") 64 | } 65 | -------------------------------------------------------------------------------- /xcodecommand/xcpretty_test.go: -------------------------------------------------------------------------------- 1 | package xcodecommand 2 | 3 | import ( 4 | "testing" 5 | 6 | gocommand "github.com/bitrise-io/go-utils/v2/command" 7 | "github.com/bitrise-io/go-utils/v2/log" 8 | mockcommand "github.com/bitrise-io/go-xcode/v2/mocks" 9 | "github.com/hashicorp/go-version" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type testingMocks struct { 14 | command *mockcommand.Command 15 | xcpretty *mockXcprettyManager 16 | } 17 | 18 | func Test_GivenNotInstalled_WhenInstall_ThenInstallsIt(t *testing.T) { 19 | // Given 20 | installer, version, mocks := createInstallerAndMocks(t, false) 21 | 22 | // When 23 | installedVersion, err := installer.CheckInstall() 24 | 25 | // Then 26 | assert.NoError(t, err) 27 | assert.Equal(t, version, installedVersion) 28 | mocks.xcpretty.AssertCalled(t, "isDepInstalled") 29 | mocks.xcpretty.AssertCalled(t, "installDep") 30 | mocks.xcpretty.AssertCalled(t, "depVersion") 31 | mocks.command.AssertCalled(t, "Run") 32 | } 33 | 34 | func Test_GivenInstalled_WhenInstall_OnlyReturnsVersion(t *testing.T) { 35 | // Given 36 | installer, version, mocks := createInstallerAndMocks(t, true) 37 | 38 | // When 39 | installedVersion, err := installer.CheckInstall() 40 | 41 | // Then 42 | assert.NoError(t, err) 43 | assert.Equal(t, version, installedVersion) 44 | mocks.xcpretty.AssertCalled(t, "isDepInstalled") 45 | mocks.xcpretty.AssertNotCalled(t, "installDep") 46 | mocks.xcpretty.AssertCalled(t, "depVersion") 47 | mocks.command.AssertNotCalled(t, "Run") 48 | } 49 | 50 | func createInstallerAndMocks(t *testing.T, installed bool) (Runner, *version.Version, testingMocks) { 51 | command := new(mockcommand.Command) 52 | command.On("Run").Return(nil) 53 | 54 | version, _ := version.NewVersion("1.0.0") 55 | 56 | mockxcpretty := newMockXcprettyManager(t) 57 | mockxcpretty.On("isDepInstalled").Return(installed, nil) 58 | if !installed { 59 | mockxcpretty.On("installDep").Return([]gocommand.Command{command}, nil) 60 | } 61 | mockxcpretty.On("depVersion").Return(version, nil) 62 | 63 | installer := &XcprettyCommandRunner{ 64 | logger: log.NewLogger(), 65 | xcpretty: mockxcpretty, 66 | } 67 | 68 | return installer, version, testingMocks{ 69 | command: command, 70 | xcpretty: mockxcpretty, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /zip/default_reader.go: -------------------------------------------------------------------------------- 1 | package zip 2 | 3 | import ( 4 | "github.com/bitrise-io/go-utils/v2/log" 5 | "github.com/bitrise-io/go-xcode/v2/internal/zip" 6 | ) 7 | 8 | // DefaultReader is a zip reader, that utilises zip.StdlibRead and zip.DittoReader readers. 9 | // If zip.StdlibRead.ReadFile fails it falls back to zip.DittoReader.ReadFile. 10 | type DefaultReader struct { 11 | logger log.Logger 12 | 13 | stdlibZipReader *zip.StdlibRead 14 | dittoZipReader *zip.DittoReader 15 | useFallbackZipReader bool 16 | } 17 | 18 | // NewDefaultReader ... 19 | func NewDefaultReader(archivePath string, logger log.Logger) (*DefaultReader, error) { 20 | stdlibReader, err := zip.NewStdlibRead(archivePath, logger) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | var dittoReader *zip.DittoReader 26 | if zip.IsDittoReaderAvailable() { 27 | dittoReader = zip.NewDittoReader(archivePath, logger) 28 | } 29 | 30 | return &DefaultReader{ 31 | logger: logger, 32 | stdlibZipReader: stdlibReader, 33 | dittoZipReader: dittoReader, 34 | useFallbackZipReader: false, 35 | }, nil 36 | } 37 | 38 | // ReadFile ... 39 | func (r *DefaultReader) ReadFile(relPthPattern string) ([]byte, error) { 40 | if !r.useFallbackZipReader { 41 | b, err := r.stdlibZipReader.ReadFile(relPthPattern) 42 | if err != nil { 43 | if zip.IsErrFormat(err) { 44 | r.logger.Warnf("stdlib zip reader failed to read %s: %s", relPthPattern, err) 45 | r.logger.Warnf("Retrying with ditto zip reader...") 46 | 47 | r.useFallbackZipReader = true 48 | return r.ReadFile(relPthPattern) 49 | } 50 | return nil, err 51 | } 52 | 53 | return b, nil 54 | } else { 55 | return r.dittoZipReader.ReadFile(relPthPattern) 56 | } 57 | } 58 | 59 | // Close ... 60 | func (r *DefaultReader) Close() error { 61 | stdlibZipReaderCloseErr := r.stdlibZipReader.Close() 62 | if !r.useFallbackZipReader { 63 | return stdlibZipReaderCloseErr 64 | } 65 | 66 | dittoZipReaderCloseErr := r.dittoZipReader.Close() 67 | if dittoZipReaderCloseErr == nil { 68 | return stdlibZipReaderCloseErr 69 | } 70 | 71 | // ditto reader's close has failed 72 | if stdlibZipReaderCloseErr != nil { 73 | r.logger.Warnf("failed to close stdlib zip reader: %s", stdlibZipReaderCloseErr) 74 | } 75 | 76 | return dittoZipReaderCloseErr 77 | } 78 | -------------------------------------------------------------------------------- /xcodecache/derived_data_path_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/bitrise-io/go-utils/pathutil" 8 | ) 9 | 10 | func Test_xcodeProjectHash(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | path string 14 | want string 15 | wantErr bool 16 | }{ 17 | { 18 | name: "normal xcodeproj path", 19 | path: "/Users/bitrise/git/sample-swiftpm.xcodeproj", 20 | want: "dsvyrfhmubmjkdguolhekiuetuie", 21 | wantErr: false, 22 | }, 23 | { 24 | name: "normal xcworkspace path", 25 | path: "/Users/bitrise/Develop/samples/sample-apps-ios-swiftpm/sample-swiftpm.xcworkspace", 26 | want: "domyjojidpnjraaljgmxofiwqhps", 27 | wantErr: false, 28 | }, 29 | { 30 | name: "Unicode composite character in path", 31 | path: "/Users/bitrise/Develop/samples/Gdańsk/sample-swiftpm.xcodeproj", 32 | want: "djfhdbzbhhqfklgywrqyqnyflvnl", 33 | wantErr: false, 34 | }, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | got, err := xcodeDerivedDataHash(tt.path) 39 | if (err != nil) != tt.wantErr { 40 | t.Errorf("xcodeProjectHash() error = %v, wantErr %v", err, tt.wantErr) 41 | return 42 | } 43 | if got != tt.want { 44 | t.Errorf("xcodeProjectHash() = %v, want %v", got, tt.want) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func Test_xcodeProjectDerivedDataPath(t *testing.T) { 51 | tests := []struct { 52 | name string 53 | projectPath string 54 | want string 55 | wantErr bool 56 | }{ 57 | { 58 | name: "normal xcodeproj path", 59 | projectPath: "/Users/bitrise/Develop/samples/sample-apps-ios-swiftpm/sample-swiftpm.xcodeproj", 60 | want: filepath.Join(pathutil.UserHomeDir(), "Library/Developer/Xcode/DerivedData/sample-swiftpm-asravewhgfsybjdvvequtqgqhbea"), 61 | wantErr: false, 62 | }, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | got, err := xcodeProjectDerivedDataPath(tt.projectPath) 67 | if (err != nil) != tt.wantErr { 68 | t.Errorf("xcodeProjectDerivedDataPath() error = %v, wantErr %v", err, tt.wantErr) 69 | return 70 | } 71 | if got != tt.want { 72 | t.Errorf("xcodeProjectDerivedDataPath() = %v, want %v", got, tt.want) 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/spaceship/devices.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship' 2 | require_relative 'log' 3 | 4 | def list_devices 5 | devices = Spaceship::Portal.device.all(mac: false, include_disabled: false) || [] 6 | 7 | devices_info = [] 8 | devices.each do |d| 9 | devices_info.append( 10 | { 11 | id: d.id, 12 | udid: d.udid, 13 | name: d.name, 14 | model: d.model, 15 | status: map_device_status_to_api_status(d.status), 16 | platform: map_device_platform_to_api_platform(d.platform), 17 | class: map_device_class_to_api_class(d.device_type) 18 | } 19 | ) 20 | end 21 | 22 | devices_info 23 | end 24 | 25 | def register_device(udid, name) 26 | device = Spaceship::Portal.device.create!(name: name, udid: udid) 27 | { 28 | device: { 29 | id: device.id, 30 | udid: device.udid, 31 | name: device.name, 32 | model: device.model, 33 | status: map_device_status_to_api_status(device.status), 34 | platform: map_device_platform_to_api_platform(device.platform), 35 | class: map_device_class_to_api_class(device.device_type) 36 | } 37 | } 38 | rescue Spaceship::UnexpectedResponse, Spaceship::BasicPreferredInfoError => e 39 | message = preferred_error_message(e) 40 | { warnings: ["Failed to register device with name: #{name} udid: #{udid} error: #{message}"] } 41 | rescue 42 | { warnings: ["Failed to register device with name: #{name} udid: #{udid}"] } 43 | end 44 | 45 | def map_device_platform_to_api_platform(platform) 46 | case platform 47 | when 'ios' 48 | 'IOS' 49 | when 'mac' 50 | 'MAC_OS' 51 | else 52 | raise "unknown device platform #{platform}" 53 | end 54 | end 55 | 56 | def map_device_status_to_api_status(status) 57 | case status 58 | when 'c' 59 | 'ENABLED' 60 | when 'r' 61 | 'DISABLED' 62 | # pending 63 | when 'p' 64 | 'DISABLED' 65 | else 66 | raise "invalid device status #{status}" 67 | end 68 | end 69 | 70 | def map_device_class_to_api_class(device_type) 71 | case device_type 72 | when 'iphone' 73 | 'IPHONE' 74 | when 'watch' 75 | 'APPLE_WATCH' 76 | when 'tvOS' 77 | 'APPLE_TV' 78 | when 'ipad' 79 | 'IPAD' 80 | when 'ipod' 81 | 'IPOD' 82 | else 83 | raise "unsupported device class #{device_type}" 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/appstoreconnect/roundtripper.go: -------------------------------------------------------------------------------- 1 | package appstoreconnect 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | // trackingRoundTripper wraps an http.RoundTripper and tracks metrics for each HTTP attempt, 10 | // including retries. It measures per-attempt duration without retry wait times included, 11 | // allowing accurate tracking even when requests are retried due to rate limits or other errors. 12 | type trackingRoundTripper struct { 13 | wrapped http.RoundTripper 14 | tracker Tracker 15 | } 16 | 17 | // isRetryContextKey is used to store whether a request is a retry attempt in the request context. 18 | type isRetryContextKey struct{} 19 | 20 | func newTrackingRoundTripper(wrapped http.RoundTripper, tracker Tracker) *trackingRoundTripper { 21 | if wrapped == nil { 22 | wrapped = http.DefaultTransport 23 | } 24 | return &trackingRoundTripper{ 25 | wrapped: wrapped, 26 | tracker: tracker, 27 | } 28 | } 29 | 30 | // markAsRetry stores a flag in the request context indicating this is a retry attempt. 31 | // It returns a new request with the updated context. This approach avoids shared state 32 | // between concurrent requests (e.g., multiple POSTs to the same endpoint with different bodies). 33 | func (t *trackingRoundTripper) markAsRetry(req *http.Request) *http.Request { 34 | ctx := context.WithValue(req.Context(), isRetryContextKey{}, true) 35 | return req.WithContext(ctx) 36 | } 37 | 38 | // RoundTrip executes an HTTP request and tracks its duration and retry status. 39 | // Each HTTP attempt (including retries) generates a separate metric event, allowing 40 | // accurate alerting based on individual response times rather than aggregate times 41 | // that include retry backoff delays. 42 | func (t *trackingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 43 | // Check if this request was marked as a retry by RequestLogHook 44 | isRetry := req.Context().Value(isRetryContextKey{}) != nil 45 | 46 | startTime := time.Now() 47 | resp, err := t.wrapped.RoundTrip(req) 48 | duration := time.Since(startTime) 49 | 50 | statusCode := 0 51 | if resp != nil { 52 | statusCode = resp.StatusCode 53 | } 54 | 55 | // Track this attempt with its actual duration (no retry waits included) 56 | t.tracker.TrackAPIRequest(req.Method, req.URL.Host, req.URL.Path, statusCode, duration, isRetry) 57 | 58 | return resp, err 59 | } 60 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/appstoreconnect/error.go: -------------------------------------------------------------------------------- 1 | package appstoreconnect 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // ErrorResponseError ... 10 | type ErrorResponseError struct { 11 | Code string `json:"code,omitempty"` 12 | Status string `json:"status,omitempty"` 13 | ID string `json:"id,omitempty"` 14 | Title string `json:"title,omitempty"` 15 | Detail string `json:"detail,omitempty"` 16 | Source interface{} `json:"source,omitempty"` 17 | } 18 | 19 | // ErrorResponse ... 20 | type ErrorResponse struct { 21 | Response *http.Response 22 | Errors []ErrorResponseError `json:"errors,omitempty"` 23 | } 24 | 25 | // Error ... 26 | func (r ErrorResponse) Error() string { 27 | var m string 28 | if r.Response.Request != nil { 29 | m = fmt.Sprintf("%s %s: %d\n", r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode) 30 | } 31 | 32 | var s string 33 | for _, err := range r.Errors { 34 | m += s + fmt.Sprintf("- %s: %s: %s", err.Code, err.Title, err.Detail) 35 | s = "\n" 36 | } 37 | 38 | return m 39 | } 40 | 41 | // IsCursorInvalid ... 42 | func (r ErrorResponse) IsCursorInvalid() bool { 43 | // {"errors"=>[{"id"=>"[ ... ]", "status"=>"400", "code"=>"PARAMETER_ERROR.INVALID", "title"=>"A parameter has an invalid value", "detail"=>"'eyJvZmZzZXQiOiIyMCJ9' is not a valid cursor for this request", "source"=>{"parameter"=>"cursor"}}]} 44 | for _, err := range r.Errors { 45 | if err.Code == "PARAMETER_ERROR.INVALID" && strings.Contains(err.Detail, "is not a valid cursor for this request") { 46 | return true 47 | } 48 | } 49 | return false 50 | } 51 | 52 | // IsRequiredAgreementMissingOrExpired ... 53 | func (r ErrorResponse) IsRequiredAgreementMissingOrExpired() bool { 54 | // status code: 403 55 | // code: FORBIDDEN.REQUIRED_AGREEMENTS_MISSING_OR_EXPIRED 56 | // title: A required agreement is missing or has expired. 57 | // detail: This request requires an in-effect agreement that has not been signed or has expired. 58 | 59 | for _, err := range r.Errors { 60 | if err.Code == "FORBIDDEN.REQUIRED_AGREEMENTS_MISSING_OR_EXPIRED" { 61 | return true 62 | } 63 | } 64 | 65 | return false 66 | } 67 | 68 | // DeviceRegistrationError ... 69 | type DeviceRegistrationError struct { 70 | Reason string 71 | } 72 | 73 | // Error ... 74 | func (e DeviceRegistrationError) Error() string { 75 | return e.Reason 76 | } 77 | -------------------------------------------------------------------------------- /_integration_tests/appstoreconnect_tests/api_key_credential_helper.go: -------------------------------------------------------------------------------- 1 | package appstoreconnect_tests 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "testing" 7 | 8 | "github.com/bitrise-io/go-utils/v2/log" 9 | "github.com/bitrise-io/go-utils/v2/retryhttp" 10 | "github.com/bitrise-io/go-xcode/v2/_integration_tests" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func getAPIKey(t *testing.T) (string, string, []byte, bool) { 15 | if os.Getenv("TEST_API_KEY") != "" { 16 | return getLocalAPIKey(t) 17 | } 18 | return getRemoteAPIKey(t) 19 | } 20 | 21 | func getLocalAPIKey(t *testing.T) (string, string, []byte, bool) { 22 | keyID := os.Getenv("TEST_API_KEY_ID") 23 | require.NotEmpty(t, keyID) 24 | issuerID := os.Getenv("TEST_API_KEY_ISSUER_ID") 25 | require.NotEmpty(t, issuerID) 26 | privateKey := os.Getenv("TEST_API_KEY") 27 | require.NotEmpty(t, privateKey) 28 | isEnterpriseAPIKey := os.Getenv("TEST_API_KEY_IS_ENTERPRISE") == "true" 29 | 30 | return keyID, issuerID, []byte(privateKey), isEnterpriseAPIKey 31 | } 32 | 33 | func getRemoteAPIKey(t *testing.T) (string, string, []byte, bool) { 34 | serviceAccountJSON := os.Getenv("GCS_SERVICE_ACCOUNT_JSON") 35 | require.NotEmpty(t, serviceAccountJSON) 36 | projectID := os.Getenv("GCS_PROJECT_ID") 37 | require.NotEmpty(t, projectID) 38 | bucketName := os.Getenv("GCS_BUCKET_NAME") 39 | require.NotEmpty(t, bucketName) 40 | 41 | secretAccessor, err := _integration_tests.NewSecretAccessor(serviceAccountJSON, projectID) 42 | require.NoError(t, err) 43 | 44 | bucketAccessor, err := _integration_tests.NewBucketAccessor(serviceAccountJSON, bucketName) 45 | require.NoError(t, err) 46 | 47 | keyID, err := secretAccessor.GetSecret("BITRISE_APPSTORECONNECT_API_KEY_ID") 48 | require.NoError(t, err) 49 | 50 | issuerID, err := secretAccessor.GetSecret("BITRISE_APPSTORECONNECT_API_KEY_ISSUER_ID") 51 | require.NoError(t, err) 52 | 53 | keyURL, err := secretAccessor.GetSecret("BITRISE_APPSTORECONNECT_API_KEY_URL") 54 | require.NoError(t, err) 55 | 56 | keyDownloadURL, err := bucketAccessor.GetExpiringURL(keyURL) 57 | require.NoError(t, err) 58 | 59 | logger := log.NewLogger() 60 | logger.EnableDebugLog(false) 61 | client := retryhttp.NewClient(logger) 62 | resp, err := client.Get(keyDownloadURL) 63 | require.NoError(t, err) 64 | 65 | privateKey, err := io.ReadAll(resp.Body) 66 | require.NoError(t, err) 67 | 68 | return keyID, issuerID, privateKey, false 69 | } 70 | -------------------------------------------------------------------------------- /xcodecommand/mock_xcprettyManager.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.13.1. DO NOT EDIT. 2 | 3 | package xcodecommand 4 | 5 | import ( 6 | command "github.com/bitrise-io/go-utils/v2/command" 7 | mock "github.com/stretchr/testify/mock" 8 | 9 | version "github.com/hashicorp/go-version" 10 | ) 11 | 12 | // mockXcprettyManager is an autogenerated mock type for the xcprettyManager type 13 | type mockXcprettyManager struct { 14 | mock.Mock 15 | } 16 | 17 | // depVersion provides a mock function with given fields: 18 | func (_m *mockXcprettyManager) depVersion() (*version.Version, error) { 19 | ret := _m.Called() 20 | 21 | var r0 *version.Version 22 | if rf, ok := ret.Get(0).(func() *version.Version); ok { 23 | r0 = rf() 24 | } else { 25 | if ret.Get(0) != nil { 26 | r0 = ret.Get(0).(*version.Version) 27 | } 28 | } 29 | 30 | var r1 error 31 | if rf, ok := ret.Get(1).(func() error); ok { 32 | r1 = rf() 33 | } else { 34 | r1 = ret.Error(1) 35 | } 36 | 37 | return r0, r1 38 | } 39 | 40 | // installDep provides a mock function with given fields: 41 | func (_m *mockXcprettyManager) installDep() []command.Command { 42 | ret := _m.Called() 43 | 44 | var r0 []command.Command 45 | if rf, ok := ret.Get(0).(func() []command.Command); ok { 46 | r0 = rf() 47 | } else { 48 | if ret.Get(0) != nil { 49 | r0 = ret.Get(0).([]command.Command) 50 | } 51 | } 52 | 53 | return r0 54 | } 55 | 56 | // isDepInstalled provides a mock function with given fields: 57 | func (_m *mockXcprettyManager) isDepInstalled() (bool, error) { 58 | ret := _m.Called() 59 | 60 | var r0 bool 61 | if rf, ok := ret.Get(0).(func() bool); ok { 62 | r0 = rf() 63 | } else { 64 | r0 = ret.Get(0).(bool) 65 | } 66 | 67 | var r1 error 68 | if rf, ok := ret.Get(1).(func() error); ok { 69 | r1 = rf() 70 | } else { 71 | r1 = ret.Error(1) 72 | } 73 | 74 | return r0, r1 75 | } 76 | 77 | type mockConstructorTestingTnewMockXcprettyManager interface { 78 | mock.TestingT 79 | Cleanup(func()) 80 | } 81 | 82 | // newMockXcprettyManager creates a new instance of mockXcprettyManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 83 | func newMockXcprettyManager(t mockConstructorTestingTnewMockXcprettyManager) *mockXcprettyManager { 84 | mock := &mockXcprettyManager{} 85 | mock.Mock.Test(t) 86 | 87 | t.Cleanup(func() { mock.AssertExpectations(t) }) 88 | 89 | return mock 90 | } 91 | -------------------------------------------------------------------------------- /exportoptionsgenerator/profiles.go: -------------------------------------------------------------------------------- 1 | package exportoptionsgenerator 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/bitrise-io/go-utils/pathutil" 10 | "github.com/bitrise-io/go-utils/v2/log" 11 | "github.com/bitrise-io/go-xcode/profileutil" 12 | ) 13 | 14 | // ProvisioningProfileProvider can list profile infos. 15 | type ProvisioningProfileProvider interface { 16 | ListProvisioningProfiles() ([]profileutil.ProvisioningProfileInfoModel, error) 17 | GetDefaultProvisioningProfile() (profileutil.ProvisioningProfileInfoModel, error) 18 | } 19 | 20 | // LocalProvisioningProfileProvider ... 21 | type LocalProvisioningProfileProvider struct { 22 | logger log.Logger 23 | } 24 | 25 | // ListProvisioningProfiles ... 26 | func (p LocalProvisioningProfileProvider) ListProvisioningProfiles() ([]profileutil.ProvisioningProfileInfoModel, error) { 27 | return profileutil.InstalledProvisioningProfileInfos(profileutil.ProfileTypeIos) 28 | } 29 | 30 | // GetDefaultProvisioningProfile ... 31 | func (p LocalProvisioningProfileProvider) GetDefaultProvisioningProfile() (profileutil.ProvisioningProfileInfoModel, error) { 32 | defaultProfileURL := os.Getenv("BITRISE_DEFAULT_PROVISION_URL") 33 | if defaultProfileURL == "" { 34 | return profileutil.ProvisioningProfileInfoModel{}, nil 35 | } 36 | 37 | tmpDir, err := pathutil.NormalizedOSTempDirPath("tmp_default_profile") 38 | if err != nil { 39 | return profileutil.ProvisioningProfileInfoModel{}, err 40 | } 41 | 42 | tmpDst := filepath.Join(tmpDir, "default.mobileprovision") 43 | tmpDstFile, err := os.Create(tmpDst) 44 | if err != nil { 45 | return profileutil.ProvisioningProfileInfoModel{}, err 46 | } 47 | defer func() { 48 | if err := tmpDstFile.Close(); err != nil { 49 | p.logger.Warnf("Failed to close file (%s), error: %s", tmpDst, err) 50 | } 51 | }() 52 | 53 | response, err := http.Get(defaultProfileURL) 54 | if err != nil { 55 | return profileutil.ProvisioningProfileInfoModel{}, err 56 | } 57 | defer func() { 58 | if err := response.Body.Close(); err != nil { 59 | p.logger.Warnf("Failed to close response body, error: %s", err) 60 | } 61 | }() 62 | 63 | if _, err := io.Copy(tmpDstFile, response.Body); err != nil { 64 | return profileutil.ProvisioningProfileInfoModel{}, err 65 | } 66 | 67 | defaultProfile, err := profileutil.NewProvisioningProfileInfoFromFile(tmpDst) 68 | if err != nil { 69 | return profileutil.ProvisioningProfileInfoModel{}, err 70 | } 71 | 72 | return defaultProfile, nil 73 | } 74 | -------------------------------------------------------------------------------- /errorfinder/errorfinder.go: -------------------------------------------------------------------------------- 1 | package errorfinder 2 | 3 | import ( 4 | "bufio" 5 | "strings" 6 | ) 7 | 8 | // FindXcodebuildErrors ... 9 | func FindXcodebuildErrors(out string) []string { 10 | var errorLines []string // single line errors with "error: " prefix 11 | var xcodebuildErrors []string // multiline errors starting with "xcodebuild: error: " prefix 12 | var nserrors []nsError // single line NSErrors with schema: Error Domain= Code= "" UserInfo= 13 | 14 | isXcodebuildError := false 15 | var xcodebuildError string 16 | 17 | scanner := bufio.NewScanner(strings.NewReader(out)) 18 | scanner.Split(bufio.ScanLines) 19 | for scanner.Scan() { 20 | line := scanner.Text() 21 | 22 | if isXcodebuildError { 23 | line = strings.TrimLeft(line, " ") 24 | if strings.HasPrefix(line, "Reason: ") || strings.HasPrefix(line, "Recovery suggestion: ") { 25 | xcodebuildError += "\n" + line 26 | continue 27 | } else { 28 | xcodebuildErrors = append(xcodebuildErrors, xcodebuildError) 29 | xcodebuildError = "" 30 | isXcodebuildError = false 31 | } 32 | } 33 | 34 | switch { 35 | case strings.HasPrefix(line, "xcodebuild: error: "): 36 | xcodebuildError = line 37 | isXcodebuildError = true 38 | case strings.HasPrefix(line, "error: ") || strings.Contains(line, " error: "): 39 | errorLines = append(errorLines, line) 40 | case strings.HasPrefix(line, "Error "): 41 | if e := newNSError(line); e != nil { 42 | nserrors = append(nserrors, *e) 43 | } 44 | } 45 | } 46 | if err := scanner.Err(); err != nil { 47 | return nil 48 | } 49 | 50 | if xcodebuildError != "" { 51 | xcodebuildErrors = append(xcodebuildErrors, xcodebuildError) 52 | } 53 | 54 | // Regular error lines (line with 'error: ' prefix) seems to have 55 | // NSError line pairs (same description) in some cases. 56 | errorLines = intersection(errorLines, nserrors) 57 | 58 | return append(errorLines, xcodebuildErrors...) 59 | } 60 | 61 | func intersection(errorLines []string, nserrors []nsError) []string { 62 | union := make([]string, len(errorLines)) 63 | copy(union, errorLines) 64 | 65 | for _, nserror := range nserrors { 66 | found := false 67 | for i, errorLine := range errorLines { 68 | // Checking suffix, as regular error lines have additional prefixes, like "error: exportArchive: " 69 | if strings.HasSuffix(errorLine, nserror.Description) { 70 | union[i] = nserror.Error() 71 | found = true 72 | break 73 | } 74 | } 75 | if !found { 76 | union = append(union, nserror.Error()) 77 | } 78 | } 79 | return union 80 | } 81 | -------------------------------------------------------------------------------- /autocodesign/errors.go: -------------------------------------------------------------------------------- 1 | package autocodesign 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" 7 | ) 8 | 9 | // DetailedError ... 10 | type DetailedError struct { 11 | ErrorMessage string 12 | Title string 13 | Description string 14 | Recommendation string 15 | } 16 | 17 | func (e DetailedError) Error() string { 18 | message := "" 19 | if e.ErrorMessage != "" { 20 | message += e.ErrorMessage + "\n" 21 | } 22 | message += "\n" 23 | if e.Title != "" { 24 | message += e.Title + "\n" 25 | } 26 | if e.Description != "" { 27 | message += e.Description + "\n" 28 | } 29 | if e.Recommendation != "" { 30 | message += "\n" 31 | message += e.Recommendation + "\n" 32 | } 33 | 34 | return message 35 | } 36 | 37 | // missingCertificateError ... 38 | type missingCertificateError struct { 39 | Type appstoreconnect.CertificateType 40 | } 41 | 42 | func (e missingCertificateError) Error() string { 43 | return fmt.Sprintf("no valid %s type certificates uploaded\n ", e.Type) 44 | } 45 | 46 | // NonmatchingProfileError is returned when a profile/bundle ID does not match project requirements 47 | // It is not a fatal error, as the profile can be regenerated 48 | type NonmatchingProfileError struct { 49 | Reason string 50 | } 51 | 52 | func (e NonmatchingProfileError) Error() string { 53 | return fmt.Sprintf("provisioning profile does not match requirements: %s", e.Reason) 54 | } 55 | 56 | // ProfilesInconsistentError is returned when a profile is deleted by an other actor 57 | type ProfilesInconsistentError struct { 58 | wrapErr error 59 | } 60 | 61 | // NewProfilesInconsistentError ... 62 | func NewProfilesInconsistentError(wrapErr error) ProfilesInconsistentError { 63 | return ProfilesInconsistentError{ 64 | wrapErr: wrapErr, 65 | } 66 | } 67 | 68 | func (e ProfilesInconsistentError) Error() string { 69 | return fmt.Sprintf("provisioning profiles were concurrently changed on Developer Portal, %s", e.wrapErr) 70 | } 71 | 72 | func (e ProfilesInconsistentError) Unwrap() error { 73 | return e.wrapErr 74 | } 75 | 76 | // ErrAppClipAppID ... 77 | type ErrAppClipAppID struct { 78 | } 79 | 80 | // Error ... 81 | func (ErrAppClipAppID) Error() string { 82 | return "can't create Application Identifier for App Clip target" 83 | } 84 | 85 | // ErrAppClipAppIDWithAppleSigning ... 86 | type ErrAppClipAppIDWithAppleSigning struct { 87 | } 88 | 89 | // Error ... 90 | func (ErrAppClipAppIDWithAppleSigning) Error() string { 91 | return "can't manage Application Identifier for App Clip target with 'Sign In With Apple' capability" 92 | } 93 | -------------------------------------------------------------------------------- /autocodesign/localcodesignasset/profile.go: -------------------------------------------------------------------------------- 1 | package localcodesignasset 2 | 3 | import ( 4 | "github.com/bitrise-io/go-xcode/profileutil" 5 | "github.com/bitrise-io/go-xcode/v2/autocodesign" 6 | "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" 7 | "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/time" 8 | ) 9 | 10 | // Profile ... 11 | type Profile struct { 12 | attributes appstoreconnect.ProfileAttributes 13 | id string 14 | bundleID string 15 | deviceUDIDs []string 16 | certificateIDs []string 17 | } 18 | 19 | // NewProfile wraps a local profile in the autocodesign.Profile interface 20 | func NewProfile(info profileutil.ProvisioningProfileInfoModel, content []byte) autocodesign.Profile { 21 | return Profile{ 22 | attributes: appstoreconnect.ProfileAttributes{ 23 | Name: info.Name, 24 | UUID: info.UUID, 25 | ProfileContent: content, 26 | Platform: getBundleIDPlatform(info.Type), 27 | ExpirationDate: time.Time(info.ExpirationDate), 28 | }, 29 | id: "", // only in case of Developer Portal Profiles 30 | bundleID: info.BundleID, 31 | certificateIDs: nil, // only in case of Developer Portal Profiles 32 | deviceUDIDs: nil, // this is technically avaialble from the profile content, but we don't need it 33 | } 34 | } 35 | 36 | // ID ... 37 | func (p Profile) ID() string { 38 | return p.id 39 | } 40 | 41 | // Attributes ... 42 | func (p Profile) Attributes() appstoreconnect.ProfileAttributes { 43 | return p.attributes 44 | } 45 | 46 | // CertificateIDs ... 47 | func (p Profile) CertificateIDs() ([]string, error) { 48 | return p.certificateIDs, nil 49 | } 50 | 51 | // DeviceUDIDs ... 52 | func (p Profile) DeviceUDIDs() ([]string, error) { 53 | return p.deviceUDIDs, nil 54 | } 55 | 56 | // BundleID ... 57 | func (p Profile) BundleID() (appstoreconnect.BundleID, error) { 58 | return appstoreconnect.BundleID{ 59 | ID: p.id, 60 | Attributes: appstoreconnect.BundleIDAttributes{ 61 | Identifier: p.bundleID, 62 | Name: p.attributes.Name, 63 | }, 64 | }, nil 65 | } 66 | 67 | // Entitlements ... 68 | func (p Profile) Entitlements() (autocodesign.Entitlements, error) { 69 | return autocodesign.ParseRawProfileEntitlements(p.attributes.ProfileContent) 70 | } 71 | 72 | func getBundleIDPlatform(profileType profileutil.ProfileType) appstoreconnect.BundleIDPlatform { 73 | switch profileType { 74 | case profileutil.ProfileTypeIos, profileutil.ProfileTypeTvOs: 75 | return appstoreconnect.IOS 76 | case profileutil.ProfileTypeMacOs: 77 | return appstoreconnect.MacOS 78 | } 79 | 80 | return "" 81 | } 82 | -------------------------------------------------------------------------------- /xcconfig/xcconfig_test.go: -------------------------------------------------------------------------------- 1 | package xcconfig 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/bitrise-io/go-xcode/v2/mocks" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_WhenWritingXCConfigContent_ThenItShouldReturnFilePath(t *testing.T) { 14 | // Given 15 | var ( 16 | testContent = "TEST" 17 | testTempDir = "temp_dir" 18 | expectedPath = filepath.Join(testTempDir, "temp.xcconfig") 19 | mockPathModifier = mocks.NewPathModifier(t) 20 | mockPathChecker = mocks.NewPathChecker(t) 21 | mockPathProvider = mocks.NewPathProvider(t) 22 | mockFileManager = mocks.NewFileManager(t) 23 | ) 24 | 25 | mockPathProvider.On("CreateTempDir", "").Return(testTempDir, nil) 26 | mockFileManager.On("Write", expectedPath, testContent, fs.FileMode(0644)).Return(nil) 27 | xcconfigWriter := NewWriter(mockPathProvider, mockFileManager, mockPathChecker, mockPathModifier) 28 | 29 | // When 30 | path, err := xcconfigWriter.Write(testContent) 31 | 32 | // Then 33 | if assert.NoError(t, err) { 34 | assert.Equal(t, expectedPath, path) 35 | } 36 | } 37 | 38 | func Test_XCConfigInput_NonExistentPathErrors(t *testing.T) { 39 | // Given 40 | var ( 41 | testContent = "TEST.xcconfig" 42 | mockPathModifier = mocks.NewPathModifier(t) 43 | mockPathChecker = mocks.NewPathChecker(t) 44 | mockPathProvider = mocks.NewPathProvider(t) 45 | mockFileManager = mocks.NewFileManager(t) 46 | ) 47 | 48 | mockPathModifier.On("AbsPath", testContent).Return(testContent, nil) 49 | mockPathChecker.On("IsPathExists", testContent).Return(false, errors.New("path does not exist")) 50 | xcconfigWriter := NewWriter(mockPathProvider, mockFileManager, mockPathChecker, mockPathModifier) 51 | 52 | // When 53 | path, err := xcconfigWriter.Write(testContent) 54 | 55 | // Then 56 | assert.Error(t, err) 57 | assert.Equal(t, path, "") 58 | } 59 | 60 | func Test_XCConfigInput_CorrectInputPathReturnSamePath(t *testing.T) { 61 | // Given 62 | var ( 63 | input = "TEST.xcconfig" 64 | mockPathModifier = mocks.NewPathModifier(t) 65 | mockPathChecker = mocks.NewPathChecker(t) 66 | mockPathProvider = mocks.NewPathProvider(t) 67 | mockFileManager = mocks.NewFileManager(t) 68 | ) 69 | 70 | mockPathModifier.On("AbsPath", input).Return(input, nil) 71 | mockPathChecker.On("IsPathExists", input).Return(true, nil) 72 | xcconfigWriter := NewWriter(mockPathProvider, mockFileManager, mockPathChecker, mockPathModifier) 73 | 74 | // When 75 | path, err := xcconfigWriter.Write(input) 76 | 77 | // Then 78 | if assert.NoError(t, err) { 79 | assert.Equal(t, path, input) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /metaparser/xcarchive.go: -------------------------------------------------------------------------------- 1 | package metaparser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/bitrise-io/go-xcode/v2/artifacts" 8 | "github.com/bitrise-io/go-xcode/v2/zip" 9 | ) 10 | 11 | // MacOSProjectIsNotSupported ... 12 | var MacOSProjectIsNotSupported = errors.New("macOS project is not supported") 13 | 14 | // ParseXCArchiveData ... 15 | func (m *Parser) ParseXCArchiveData(pth string) (*ArtifactMetadata, error) { 16 | 17 | appInfo, scheme, err := m.readXCArchiveDeploymentMeta(pth) 18 | if err != nil { 19 | return &ArtifactMetadata{ 20 | AppInfo: appInfo, 21 | Scheme: scheme, 22 | }, fmt.Errorf("failed to parse deployment info for %s: %w", pth, err) 23 | } 24 | 25 | fileSize, err := m.fileManager.FileSizeInBytes(pth) 26 | if err != nil { 27 | m.logger.Warnf("Failed to get apk size, error: %s", err) 28 | } 29 | 30 | return &ArtifactMetadata{ 31 | AppInfo: appInfo, 32 | FileSizeBytes: fileSize, 33 | Scheme: scheme, 34 | }, nil 35 | } 36 | 37 | func (m *Parser) readXCArchiveDeploymentMeta(pth string) (Info, string, error) { 38 | reader, err := zip.NewDefaultReader(pth, m.logger) 39 | if err != nil { 40 | return Info{}, "", err 41 | } 42 | defer func() { 43 | if err := reader.Close(); err != nil { 44 | m.logger.Warnf("%s", err) 45 | } 46 | }() 47 | 48 | xcarchiveReader := artifacts.NewXCArchiveReader(reader) 49 | isMacos := xcarchiveReader.IsMacOS() 50 | if isMacos { 51 | return Info{}, "", MacOSProjectIsNotSupported 52 | } 53 | archiveInfoPlist, err := xcarchiveReader.InfoPlist() 54 | if err != nil { 55 | return Info{}, "", fmt.Errorf("failed to unwrap Info.plist from xcarchive: %w", err) 56 | } 57 | 58 | iosXCArchiveReader := artifacts.NewIOSXCArchiveReader(reader) 59 | appInfoPlist, err := iosXCArchiveReader.AppInfoPlist() 60 | if err != nil { 61 | return Info{}, "", fmt.Errorf("failed to unwrap application Info.plist from xcarchive: %w", err) 62 | } 63 | 64 | appTitle, _ := appInfoPlist.GetString("CFBundleName") 65 | bundleID, _ := appInfoPlist.GetString("CFBundleIdentifier") 66 | version, _ := appInfoPlist.GetString("CFBundleShortVersionString") 67 | buildNumber, _ := appInfoPlist.GetString("CFBundleVersion") 68 | minOSVersion, _ := appInfoPlist.GetString("MinimumOSVersion") 69 | deviceFamilyList, _ := appInfoPlist.GetUInt64Array("UIDeviceFamily") 70 | scheme, _ := archiveInfoPlist.GetString("SchemeName") 71 | 72 | appInfo := Info{ 73 | AppTitle: appTitle, 74 | BundleID: bundleID, 75 | Version: version, 76 | BuildNumber: buildNumber, 77 | MinOSVersion: minOSVersion, 78 | DeviceFamilyList: deviceFamilyList, 79 | } 80 | 81 | return appInfo, scheme, nil 82 | } 83 | -------------------------------------------------------------------------------- /autocodesign/projectmanager/projectmanager_test.go: -------------------------------------------------------------------------------- 1 | package projectmanager 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bitrise-io/go-xcode/v2/autocodesign" 7 | ) 8 | 9 | func TestCanGenerateProfileWithEntitlements(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | entitlementsByBundleID map[string]autocodesign.Entitlements 13 | wantOk bool 14 | wantEntitlement string 15 | wantBundleID string 16 | }{ 17 | { 18 | name: "no entitlements", 19 | entitlementsByBundleID: map[string]autocodesign.Entitlements{ 20 | "com.bundleid": map[string]interface{}{}, 21 | }, 22 | wantOk: true, 23 | wantEntitlement: "", 24 | wantBundleID: "", 25 | }, 26 | { 27 | name: "contains unsupported entitlement", 28 | entitlementsByBundleID: map[string]autocodesign.Entitlements{ 29 | "com.bundleid": map[string]interface{}{ 30 | "com.entitlement-ignored": true, 31 | "com.apple.developer.contacts.notes": true, 32 | }, 33 | }, 34 | wantOk: false, 35 | wantEntitlement: "com.apple.developer.contacts.notes", 36 | wantBundleID: "com.bundleid", 37 | }, 38 | { 39 | name: "contains unsupported entitlement, multiple bundle IDs", 40 | entitlementsByBundleID: map[string]autocodesign.Entitlements{ 41 | "com.bundleid": map[string]interface{}{ 42 | "aps-environment": true, 43 | }, 44 | "com.bundleid2": map[string]interface{}{ 45 | "com.entitlement-ignored": true, 46 | "com.apple.developer.contacts.notes": true, 47 | }, 48 | }, 49 | wantOk: false, 50 | wantEntitlement: "com.apple.developer.contacts.notes", 51 | wantBundleID: "com.bundleid2", 52 | }, 53 | { 54 | name: "all entitlements supported", 55 | entitlementsByBundleID: map[string]autocodesign.Entitlements{ 56 | "com.bundleid": map[string]interface{}{ 57 | "aps-environment": true, 58 | }, 59 | }, 60 | wantOk: true, 61 | wantEntitlement: "", 62 | wantBundleID: "", 63 | }, 64 | } 65 | for _, tt := range tests { 66 | t.Run(tt.name, func(t *testing.T) { 67 | gotOk, gotEntilement, gotBundleID := CanGenerateProfileWithEntitlements(tt.entitlementsByBundleID) 68 | if gotOk != tt.wantOk { 69 | t.Errorf("CanGenerateProfileWithEntitlements() got = %v, want %v", gotOk, tt.wantOk) 70 | } 71 | if gotEntilement != tt.wantEntitlement { 72 | t.Errorf("CanGenerateProfileWithEntitlements() got1 = %v, want %v", gotEntilement, tt.wantEntitlement) 73 | } 74 | if gotBundleID != tt.wantBundleID { 75 | t.Errorf("CanGenerateProfileWithEntitlements() got2 = %v, want %v", gotBundleID, tt.wantBundleID) 76 | } 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /logio/pipe_wiring.go: -------------------------------------------------------------------------------- 1 | package logio 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "os" 8 | "regexp" 9 | "sync" 10 | ) 11 | 12 | // PipeWiring is a helper struct to define the setup and binding of tools and 13 | // xcbuild with a filter and stdout. It is purely boilerplate reduction and it is the 14 | // users responsibility to choose between this and manual hooking of the in/outputs. 15 | // It also provides a convenient Close() method that only closes things that can/should be closed. 16 | type PipeWiring struct { 17 | XcbuildRawout *bytes.Buffer 18 | XcbuildStdout io.Writer 19 | XcbuildStderr io.Writer 20 | ToolStdin io.ReadCloser 21 | ToolStdout io.WriteCloser 22 | ToolStderr io.WriteCloser 23 | 24 | toolPipeW *io.PipeWriter 25 | bufferedStdout *Sink 26 | toolInSink *Sink 27 | filter *PrefixFilter 28 | 29 | closeFilterOnce sync.Once 30 | } 31 | 32 | // CloseFilter closes the filter and waits for it to finish 33 | func (p *PipeWiring) CloseFilter() error { 34 | err := error(nil) 35 | p.closeFilterOnce.Do(func() { 36 | err = p.filter.Close() 37 | <-p.filter.Done() 38 | 39 | }) 40 | return err 41 | } 42 | 43 | // Close ... 44 | func (p *PipeWiring) Close() error { 45 | filterErr := p.CloseFilter() 46 | toolSinkErr := p.toolInSink.Close() 47 | pipeWErr := p.toolPipeW.Close() 48 | bufferedStdoutErr := p.bufferedStdout.Close() 49 | 50 | return errors.Join(filterErr, toolSinkErr, pipeWErr, bufferedStdoutErr) 51 | } 52 | 53 | // SetupPipeWiring creates a new PipeWiring instance that contains the usual 54 | // input/outputs that an xcodebuild command and a logging tool needs when we are also 55 | // using a logging filter. 56 | func SetupPipeWiring(filter *regexp.Regexp) *PipeWiring { 57 | // Create a buffer to store raw xcbuild output 58 | rawXcbuild := bytes.NewBuffer(nil) 59 | // Pipe filtered logs to tool 60 | toolPipeR, toolPipeW := io.Pipe() 61 | 62 | // Add a buffer before stdout 63 | bufferedStdout := NewSink(os.Stdout) 64 | // Add a buffer before tool input 65 | toolInSink := NewSink(toolPipeW) 66 | xcbuildLogs := io.MultiWriter(rawXcbuild, toolInSink) 67 | // Create a filter for [Bitrise ...] prefixes 68 | bitrisePrefixFilter := NewPrefixFilter( 69 | filter, 70 | bufferedStdout, 71 | xcbuildLogs, 72 | ) 73 | 74 | return &PipeWiring{ 75 | XcbuildRawout: rawXcbuild, 76 | XcbuildStdout: bitrisePrefixFilter, 77 | XcbuildStderr: bitrisePrefixFilter, 78 | ToolStdin: toolPipeR, 79 | ToolStdout: os.Stdout, 80 | ToolStderr: os.Stderr, 81 | 82 | toolPipeW: toolPipeW, 83 | bufferedStdout: bufferedStdout, 84 | toolInSink: toolInSink, 85 | filter: bitrisePrefixFilter, 86 | 87 | closeFilterOnce: sync.Once{}, 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /metaparser/ipa.go: -------------------------------------------------------------------------------- 1 | package metaparser 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bitrise-io/go-xcode/v2/artifacts" 7 | "github.com/bitrise-io/go-xcode/v2/zip" 8 | ) 9 | 10 | // ParseIPAData ... 11 | func (m *Parser) ParseIPAData(pth string) (*ArtifactMetadata, error) { 12 | appInfo, provisioningInfo, err := m.readIPADeploymentMeta(pth) 13 | if err != nil { 14 | return nil, fmt.Errorf("failed to parse deployment info for %s: %w", pth, err) 15 | } 16 | 17 | fileSize, err := m.fileManager.FileSizeInBytes(pth) 18 | if err != nil { 19 | m.logger.Warnf("Failed to get apk size, error: %s", err) 20 | } 21 | 22 | return &ArtifactMetadata{ 23 | AppInfo: appInfo, 24 | FileSizeBytes: fileSize, 25 | ProvisioningInfo: provisioningInfo, 26 | }, nil 27 | } 28 | 29 | func (m *Parser) readIPADeploymentMeta(ipaPth string) (Info, ProvisionInfo, error) { 30 | reader, err := zip.NewDefaultReader(ipaPth, m.logger) 31 | if err != nil { 32 | return Info{}, ProvisionInfo{}, err 33 | } 34 | defer func() { 35 | if err := reader.Close(); err != nil { 36 | m.logger.Warnf("%s", err) 37 | } 38 | }() 39 | 40 | ipaReader := artifacts.NewIPAReader(reader) 41 | infoPlist, err := ipaReader.AppInfoPlist() 42 | if err != nil { 43 | return Info{}, ProvisionInfo{}, fmt.Errorf("failed to unwrap Info.plist from ipa: %w", err) 44 | } 45 | 46 | appTitle, _ := infoPlist.GetString("CFBundleName") 47 | bundleID, _ := infoPlist.GetString("CFBundleIdentifier") 48 | version, _ := infoPlist.GetString("CFBundleShortVersionString") 49 | buildNumber, _ := infoPlist.GetString("CFBundleVersion") 50 | minOSVersion, _ := infoPlist.GetString("MinimumOSVersion") 51 | deviceFamilyList, _ := infoPlist.GetUInt64Array("UIDeviceFamily") 52 | 53 | appInfo := Info{ 54 | AppTitle: appTitle, 55 | BundleID: bundleID, 56 | Version: version, 57 | BuildNumber: buildNumber, 58 | MinOSVersion: minOSVersion, 59 | DeviceFamilyList: deviceFamilyList, 60 | } 61 | 62 | provisioningProfileInfo, err := ipaReader.ProvisioningProfileInfo() 63 | if err != nil { 64 | return Info{}, ProvisionInfo{}, fmt.Errorf("failed to read profile info from ipa: %w", err) 65 | } 66 | 67 | provisioningInfo := ProvisionInfo{ 68 | CreationDate: provisioningProfileInfo.CreationDate, 69 | ExpireDate: provisioningProfileInfo.ExpirationDate, 70 | DeviceUDIDList: provisioningProfileInfo.ProvisionedDevices, 71 | TeamName: provisioningProfileInfo.TeamName, 72 | ProfileName: provisioningProfileInfo.Name, 73 | ProvisionsAllDevices: provisioningProfileInfo.ProvisionsAllDevices, 74 | IPAExportMethod: provisioningProfileInfo.ExportType, 75 | } 76 | 77 | return appInfo, provisioningInfo, nil 78 | } 79 | -------------------------------------------------------------------------------- /autocodesign/utils.go: -------------------------------------------------------------------------------- 1 | package autocodesign 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/bitrise-io/go-utils/log" 8 | ) 9 | 10 | func mergeCodeSignAssets(base, additional *AppCodesignAssets) *AppCodesignAssets { 11 | if additional == nil { 12 | return base 13 | } 14 | 15 | if base == nil { 16 | return additional 17 | } 18 | 19 | if additional.ArchivableTargetProfilesByBundleID == nil { 20 | additional.ArchivableTargetProfilesByBundleID = base.ArchivableTargetProfilesByBundleID 21 | } else { 22 | for bundleID, profile := range base.ArchivableTargetProfilesByBundleID { 23 | additional.ArchivableTargetProfilesByBundleID[bundleID] = profile 24 | } 25 | } 26 | 27 | if additional.UITestTargetProfilesByBundleID == nil { 28 | additional.UITestTargetProfilesByBundleID = base.UITestTargetProfilesByBundleID 29 | } else { 30 | for bundleID, profile := range base.UITestTargetProfilesByBundleID { 31 | additional.UITestTargetProfilesByBundleID[bundleID] = profile 32 | } 33 | } 34 | 35 | base = additional 36 | 37 | return base 38 | } 39 | 40 | func printMissingCodeSignAssets(missingCodesignAssets *AppLayout) { 41 | fmt.Println() 42 | log.Infof("Local code signing assets not found for:") 43 | log.Printf("Archivable targets (%d)", len(missingCodesignAssets.EntitlementsByArchivableTargetBundleID)) 44 | for bundleID := range missingCodesignAssets.EntitlementsByArchivableTargetBundleID { 45 | log.Printf("- %s", bundleID) 46 | } 47 | log.Printf("UITest targets (%d)", len(missingCodesignAssets.UITestTargetBundleIDs)) 48 | for _, bundleID := range missingCodesignAssets.UITestTargetBundleIDs { 49 | log.Printf("- %s", bundleID) 50 | } 51 | } 52 | 53 | func printExistingCodesignAssets(assets *AppCodesignAssets, distrType DistributionType) { 54 | if assets == nil { 55 | return 56 | } 57 | 58 | fmt.Println() 59 | log.Infof("Local code signing assets for %s distribution:", distrType) 60 | log.Printf("Certificate: %s (team name: %s, serial: %s)", assets.Certificate.CommonName, assets.Certificate.TeamName, assets.Certificate.Serial) 61 | log.Printf("Archivable targets (%d)", len(assets.ArchivableTargetProfilesByBundleID)) 62 | for bundleID, profile := range assets.ArchivableTargetProfilesByBundleID { 63 | log.Printf("- %s: %s (ID: %s UUID: %s Expiry: %s)", bundleID, profile.Attributes().Name, profile.ID(), profile.Attributes().UUID, time.Time(profile.Attributes().ExpirationDate)) 64 | } 65 | 66 | log.Printf("UITest targets (%d)", len(assets.UITestTargetProfilesByBundleID)) 67 | for bundleID, profile := range assets.UITestTargetProfilesByBundleID { 68 | log.Printf("- %s: %s (ID: %s UUID: %s Expiry: %s)", bundleID, profile.Attributes().Name, profile.ID(), profile.Attributes().UUID, time.Time(profile.Attributes().ExpirationDate)) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /autocodesign/certdownloader/certdownloader.go: -------------------------------------------------------------------------------- 1 | // Package certdownloader implements a autocodesign.CertificateProvider which fetches Bitrise hosted Xcode codesigning certificates. 2 | package certdownloader 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/bitrise-io/go-steputils/input" 11 | "github.com/bitrise-io/go-utils/filedownloader" 12 | "github.com/bitrise-io/go-utils/log" 13 | "github.com/bitrise-io/go-xcode/certificateutil" 14 | "github.com/bitrise-io/go-xcode/v2/autocodesign" 15 | ) 16 | 17 | // CertificateAndPassphrase contains a p12 file URL and passphrase 18 | type CertificateAndPassphrase struct { 19 | URL, Passphrase string 20 | } 21 | 22 | type downloader struct { 23 | certs []CertificateAndPassphrase 24 | client *http.Client 25 | } 26 | 27 | // NewDownloader ... 28 | func NewDownloader(certs []CertificateAndPassphrase, client *http.Client) autocodesign.CertificateProvider { 29 | return downloader{ 30 | certs: certs, 31 | client: client, 32 | } 33 | } 34 | 35 | // GetCertificates ... 36 | func (d downloader) GetCertificates() ([]certificateutil.CertificateInfoModel, error) { 37 | var certInfos []certificateutil.CertificateInfoModel 38 | 39 | for i, p12 := range d.certs { 40 | log.Debugf("Downloading p12 file number %d from %s", i, p12.URL) 41 | 42 | certInfo, err := downloadAndParsePKCS12(d.client, p12.URL, p12.Passphrase) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | log.Debugf("Codesign identities included:\n%s", certsToString(certInfo)) 48 | certInfos = append(certInfos, certInfo...) 49 | } 50 | 51 | return certInfos, nil 52 | } 53 | 54 | // downloadAndParsePKCS12 downloads a pkcs12 format file and parses certificates and matching private keys. 55 | func downloadAndParsePKCS12(httpClient *http.Client, certificateURL, passphrase string) ([]certificateutil.CertificateInfoModel, error) { 56 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) 57 | defer cancel() 58 | 59 | downloader := filedownloader.NewWithContext(ctx, httpClient) 60 | fileProvider := input.NewFileProvider(downloader) 61 | 62 | contents, err := fileProvider.Contents(certificateURL) 63 | if err != nil { 64 | return nil, err 65 | } else if contents == nil { 66 | return nil, fmt.Errorf("certificate (%s) is empty", certificateURL) 67 | } 68 | 69 | infos, err := certificateutil.CertificatesFromPKCS12Content(contents, passphrase) 70 | if err != nil { 71 | return nil, fmt.Errorf("failed to parse certificate (%s), err: %s", certificateURL, err) 72 | } 73 | 74 | return infos, nil 75 | } 76 | 77 | func certsToString(certs []certificateutil.CertificateInfoModel) (s string) { 78 | for i, cert := range certs { 79 | s += "- " 80 | s += cert.String() 81 | if i < len(certs)-1 { 82 | s += "\n" 83 | } 84 | } 85 | return 86 | } 87 | -------------------------------------------------------------------------------- /xcarchive/macos_test.go: -------------------------------------------------------------------------------- 1 | package xcarchive 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestNewMacosArchive(t *testing.T) { 12 | macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") 13 | archive, err := NewMacosArchive(macosArchivePth) 14 | require.NoError(t, err) 15 | require.Equal(t, 5, len(archive.InfoPlist)) 16 | 17 | app := archive.Application 18 | require.Equal(t, 21, len(app.InfoPlist)) 19 | require.Equal(t, 2, len(app.Entitlements)) 20 | require.Nil(t, app.ProvisioningProfile) 21 | 22 | require.Equal(t, 1, len(app.Extensions)) 23 | extension := app.Extensions[0] 24 | require.Equal(t, 22, len(extension.InfoPlist)) 25 | require.Equal(t, 2, len(extension.Entitlements)) 26 | require.Nil(t, extension.ProvisioningProfile) 27 | } 28 | 29 | func TestMacosIsXcodeManaged(t *testing.T) { 30 | macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") 31 | archive, err := NewMacosArchive(macosArchivePth) 32 | require.NoError(t, err) 33 | 34 | require.Equal(t, false, archive.IsXcodeManaged()) 35 | } 36 | 37 | func TestMacosSigningIdentity(t *testing.T) { 38 | macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") 39 | archive, err := NewMacosArchive(macosArchivePth) 40 | require.NoError(t, err) 41 | 42 | require.Equal(t, "Mac Developer: Gödrei Krisztian (T3694PR6UJ)", archive.SigningIdentity()) 43 | } 44 | 45 | func TestMacosBundleIDEntitlementsMap(t *testing.T) { 46 | macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") 47 | archive, err := NewMacosArchive(macosArchivePth) 48 | require.NoError(t, err) 49 | 50 | bundleIDEntitlementsMap := archive.BundleIDEntitlementsMap() 51 | require.Equal(t, 2, len(bundleIDEntitlementsMap)) 52 | 53 | bundleIDs := []string{"io.bitrise.archive.Test", "io.bitrise.archive.Test.ActionExtension"} 54 | for _, bundleID := range bundleIDs { 55 | _, ok := bundleIDEntitlementsMap[bundleID] 56 | require.True(t, ok, fmt.Sprintf("%v", bundleIDEntitlementsMap)) 57 | } 58 | } 59 | 60 | func TestMacosBundleIDProfileInfoMap(t *testing.T) { 61 | macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") 62 | archive, err := NewMacosArchive(macosArchivePth) 63 | require.NoError(t, err) 64 | 65 | bundleIDProfileInfoMap := archive.BundleIDProfileInfoMap() 66 | require.Equal(t, 0, len(bundleIDProfileInfoMap)) 67 | } 68 | 69 | func TestMacosFindDSYMs(t *testing.T) { 70 | macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") 71 | archive, err := NewMacosArchive(macosArchivePth) 72 | require.NoError(t, err) 73 | 74 | appDsym, otherDsyms, err := archive.FindDSYMs() 75 | require.NoError(t, err) 76 | require.Equal(t, 1, len(appDsym)) 77 | require.Equal(t, 1, len(otherDsyms)) 78 | } 79 | -------------------------------------------------------------------------------- /destination/simulator_test.go: -------------------------------------------------------------------------------- 1 | package destination 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_GivenDestinationIsCorrect_WhenSimulatorIsCreated_ThenItReturnsCorrectSimulator(t *testing.T) { 10 | // Given 11 | destination := "platform=iOS Simulator,name=iPhone 8 Plus,OS=latest" 12 | 13 | // When 14 | simulator, err := NewSimulator(destination) 15 | 16 | // Then 17 | if assert.NoError(t, err) { 18 | expectedSimulator := &Simulator{Platform: "iOS Simulator", Name: "iPhone 8 Plus", OS: "latest"} 19 | assert.Equal(t, expectedSimulator, simulator) 20 | } 21 | } 22 | 23 | func Test_GivenDestinationIsCorrect_WhenSimulatorIsCreatedWithArchFlag_ThenItReturnsCorrectSimulator(t *testing.T) { 24 | // Given 25 | destination := "platform=iOS Simulator,name=iPhone 8 Plus,OS=latest,arch=x86_64" 26 | 27 | // When 28 | simulator, err := NewSimulator(destination) 29 | 30 | // Then 31 | if assert.NoError(t, err) { 32 | expectedSimulator := &Simulator{Platform: "iOS Simulator", Name: "iPhone 8 Plus", OS: "latest", Arch: "x86_64"} 33 | assert.Equal(t, expectedSimulator, simulator) 34 | } 35 | } 36 | 37 | func Test_GivenDestinationHasNoOS_WhenSimulatorIsCreated_ThenItReturnsSimulatorWithLatestOS(t *testing.T) { 38 | // Given 39 | destination := "platform=iOS Simulator,name=iPhone 8 Plus" 40 | 41 | // When 42 | simulator, err := NewSimulator(destination) 43 | 44 | // Then 45 | if assert.NoError(t, err) { 46 | expectedSimulator := &Simulator{Platform: "iOS Simulator", Name: "iPhone 8 Plus", OS: "latest"} 47 | assert.Equal(t, expectedSimulator, simulator) 48 | } 49 | } 50 | 51 | func Test_GivenDestinationHasNoPlatform_WhenSimulatorIsCreated_ThenItReturnsAnError(t *testing.T) { 52 | // Given 53 | destination := "name=iPhone 8 Plus,OS=latest" 54 | 55 | // When 56 | simulator, err := NewSimulator(destination) 57 | 58 | // Then 59 | if assert.Error(t, err) { 60 | assert.Nil(t, simulator) 61 | } 62 | } 63 | 64 | func Test_GivenDestinationHasNoName_WhenSimulatorIsCreated_ThenItReturnsAnError(t *testing.T) { 65 | // Given 66 | destination := "platform=iOS Simulator,OS=latest" 67 | 68 | // When 69 | simulator, err := NewSimulator(destination) 70 | 71 | // Then 72 | if assert.Error(t, err) { 73 | assert.Nil(t, simulator) 74 | } 75 | } 76 | 77 | func Test_GivenDestinationHasInvalidKey_WhenSimulatorIsCreated_ThenItReturnsAnError(t *testing.T) { 78 | // Given 79 | destination := "invalid=iOS Simulator,name=iPhone 8 Plus,OS=latest" 80 | 81 | // When 82 | simulator, err := NewSimulator(destination) 83 | 84 | // Then 85 | if assert.Error(t, err) { 86 | assert.Nil(t, simulator) 87 | } 88 | } 89 | 90 | func Test_GivenDestinationHasInvalidFormat_WhenSimulatorIsCreated_ThenItReturnsAnError(t *testing.T) { 91 | // Given 92 | destination := "platform:iOS Simulator,name:iPhone 8 Plus,OS:latest" 93 | 94 | // When 95 | simulator, err := NewSimulator(destination) 96 | 97 | // Then 98 | if assert.Error(t, err) { 99 | assert.Nil(t, simulator) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /internal/zip/ditto_reader.go: -------------------------------------------------------------------------------- 1 | package zip 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "sort" 10 | 11 | "github.com/bitrise-io/go-utils/v2/command" 12 | "github.com/bitrise-io/go-utils/v2/env" 13 | "github.com/bitrise-io/go-utils/v2/log" 14 | "github.com/bitrise-io/go-utils/v2/pathutil" 15 | ) 16 | 17 | // DittoReader ... 18 | type DittoReader struct { 19 | logger log.Logger 20 | archivePath string 21 | extractedDir string 22 | } 23 | 24 | // IsDittoReaderAvailable ... 25 | func IsDittoReaderAvailable() bool { 26 | _, err := exec.LookPath("ditto") 27 | return err == nil 28 | } 29 | 30 | // NewDittoReader ... 31 | func NewDittoReader(archivePath string, logger log.Logger) *DittoReader { 32 | return &DittoReader{ 33 | logger: logger, 34 | archivePath: archivePath, 35 | } 36 | } 37 | 38 | // ReadFile ... 39 | func (r *DittoReader) ReadFile(relPthPattern string) ([]byte, error) { 40 | if r.extractedDir == "" { 41 | if err := r.extractArchive(); err != nil { 42 | return nil, fmt.Errorf("failed to extract archive: %w", err) 43 | } 44 | } 45 | 46 | absPthPattern := filepath.Join(r.extractedDir, relPthPattern) 47 | matches, err := filepath.Glob(absPthPattern) 48 | if err != nil { 49 | return nil, fmt.Errorf("failed to find file with pattern: %s: %w", absPthPattern, err) 50 | } 51 | if len(matches) == 0 { 52 | return nil, fmt.Errorf("no file found with pattern: %s", absPthPattern) 53 | } 54 | 55 | sort.Strings(matches) 56 | 57 | pth := matches[0] 58 | f, err := os.Open(pth) 59 | if err != nil { 60 | return nil, fmt.Errorf("failed to open %s: %w", pth, err) 61 | } 62 | defer func() { 63 | if err := f.Close(); err != nil { 64 | r.logger.Warnf("Failed to close %s: %s", pth, err) 65 | } 66 | }() 67 | 68 | b, err := io.ReadAll(f) 69 | if err != nil { 70 | return nil, fmt.Errorf("failed to read %s: %w", pth, err) 71 | } 72 | 73 | return b, nil 74 | } 75 | 76 | // Close ... 77 | func (r *DittoReader) Close() error { 78 | if r.extractedDir == "" { 79 | return nil 80 | } 81 | return os.RemoveAll(r.extractedDir) 82 | } 83 | 84 | func (r *DittoReader) extractArchive() error { 85 | tmpDir, err := pathutil.NewPathProvider().CreateTempDir("ditto_reader") 86 | if err != nil { 87 | return nil 88 | } 89 | 90 | /* 91 | -x Extract the archives given as source arguments. The format 92 | is assumed to be CPIO, unless -k is given. Compressed CPIO 93 | is automatically handled. 94 | 95 | -k Create or extract from a PKZip archive instead of the 96 | default CPIO. PKZip archives should be stored in filenames 97 | ending in .zip. 98 | */ 99 | factory := command.NewFactory(env.NewRepository()) 100 | cmd := factory.Create("ditto", []string{"-x", "-k", r.archivePath, tmpDir}, nil) 101 | if out, err := cmd.RunAndReturnTrimmedCombinedOutput(); err != nil { 102 | fmt.Println(out) 103 | return err 104 | } 105 | 106 | r.extractedDir = tmpDir 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /exportoptionsgenerator/targets.go: -------------------------------------------------------------------------------- 1 | package exportoptionsgenerator 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bitrise-io/go-xcode/v2/plistutil" 7 | "github.com/bitrise-io/go-xcode/xcodeproject/serialized" 8 | "github.com/bitrise-io/go-xcode/xcodeproject/xcodeproj" 9 | "github.com/bitrise-io/go-xcode/xcodeproject/xcscheme" 10 | ) 11 | 12 | // ArchiveInfo contains the distribution bundle ID(s) and entitlements of the main target and its dependencies. 13 | type ArchiveInfo struct { 14 | AppBundleID string 15 | AppClipBundleID string 16 | EntitlementsByBundleID map[string]plistutil.PlistData 17 | } 18 | 19 | // ReadArchiveInfoFromXcodeproject reads the Bundle ID for the given scheme and configuration. 20 | func ReadArchiveInfoFromXcodeproject(xcodeProj *xcodeproj.XcodeProj, scheme *xcscheme.Scheme, configuration string) (ArchiveInfo, error) { 21 | mainTarget, err := ArchivableApplicationTarget(xcodeProj, scheme) 22 | if err != nil { 23 | return ArchiveInfo{}, err 24 | } 25 | 26 | dependentTargets := filterApplicationBundleTargets(xcodeProj.DependentTargetsOfTarget(*mainTarget)) 27 | targets := append([]xcodeproj.Target{*mainTarget}, dependentTargets...) 28 | 29 | mainTargetBundleID := "" 30 | appClipBundleID := "" 31 | entitlementsByBundleID := map[string]plistutil.PlistData{} 32 | for i, target := range targets { 33 | bundleID, err := xcodeProj.TargetBundleID(target.Name, configuration) 34 | if err != nil { 35 | return ArchiveInfo{}, fmt.Errorf("failed to get target (%s) bundle id: %s", target.Name, err) 36 | } 37 | 38 | entitlements, err := xcodeProj.TargetCodeSignEntitlements(target.Name, configuration) 39 | if err != nil && !serialized.IsKeyNotFoundError(err) { 40 | return ArchiveInfo{}, fmt.Errorf("failed to get target (%s) bundle id: %s", target.Name, err) 41 | } 42 | 43 | entitlementsByBundleID[bundleID] = plistutil.PlistData(entitlements) 44 | 45 | if target.IsAppClipProduct() { 46 | appClipBundleID = bundleID 47 | } 48 | if i == 0 { 49 | mainTargetBundleID = bundleID 50 | } 51 | } 52 | 53 | return ArchiveInfo{ 54 | AppBundleID: mainTargetBundleID, 55 | AppClipBundleID: appClipBundleID, 56 | EntitlementsByBundleID: entitlementsByBundleID, 57 | }, nil 58 | } 59 | 60 | // ArchivableApplicationTarget locate archivable app target from a given project and scheme 61 | func ArchivableApplicationTarget(xcodeProj *xcodeproj.XcodeProj, scheme *xcscheme.Scheme) (*xcodeproj.Target, error) { 62 | archiveEntry, ok := scheme.AppBuildActionEntry() 63 | if !ok { 64 | return nil, fmt.Errorf("archivable entry not found in project: %s for scheme: %s", xcodeProj.Path, scheme.Name) 65 | } 66 | 67 | mainTarget, ok := xcodeProj.Proj.Target(archiveEntry.BuildableReference.BlueprintIdentifier) 68 | if !ok { 69 | return nil, fmt.Errorf("target not found: %s", archiveEntry.BuildableReference.BlueprintIdentifier) 70 | } 71 | 72 | return &mainTarget, nil 73 | } 74 | 75 | func filterApplicationBundleTargets(targets []xcodeproj.Target) (filteredTargets []xcodeproj.Target) { 76 | for _, target := range targets { 77 | if !target.IsExecutableProduct() { 78 | continue 79 | } 80 | 81 | filteredTargets = append(filteredTargets, target) 82 | } 83 | 84 | return 85 | } 86 | -------------------------------------------------------------------------------- /autocodesign/certdownloader/certdownloader_test.go: -------------------------------------------------------------------------------- 1 | package certdownloader 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/bitrise-io/go-xcode/certificateutil" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func Test_downloader_GetCertificates_Local(t *testing.T) { 15 | certInfo := createTestCert(t) 16 | passphrase := "" 17 | 18 | certData, err := certInfo.EncodeToP12(passphrase) 19 | if err != nil { 20 | t.Errorf("init: failed to encode certificate: %s", err) 21 | } 22 | 23 | p12File, err := os.CreateTemp("", "*.p12") 24 | if err != nil { 25 | t.Errorf("init: failed to create temp test file: %s", err) 26 | } 27 | 28 | if _, err = p12File.Write(certData); err != nil { 29 | t.Errorf("init: failed to write test file: %s", err) 30 | } 31 | 32 | if err = p12File.Close(); err != nil { 33 | t.Errorf("init: failed to close file: %s", err) 34 | } 35 | 36 | p12path := "file://" + p12File.Name() 37 | 38 | d := downloader{ 39 | certs: []CertificateAndPassphrase{{ 40 | URL: p12path, 41 | Passphrase: passphrase, 42 | }}, 43 | client: http.DefaultClient, 44 | } 45 | got, err := d.GetCertificates() 46 | 47 | want := []certificateutil.CertificateInfoModel{ 48 | certInfo, 49 | } 50 | 51 | assert.NoError(t, err) 52 | assert.Equal(t, want, got) 53 | } 54 | 55 | func Test_downloader_GetCertificates_Remote(t *testing.T) { 56 | certInfo := createTestCert(t) 57 | passphrase := "" 58 | 59 | certData, err := certInfo.EncodeToP12(passphrase) 60 | if err != nil { 61 | t.Errorf("init: failed to encode certificate: %s", err) 62 | } 63 | 64 | storage := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 65 | if r.Method != http.MethodGet { 66 | w.WriteHeader(http.StatusNotFound) 67 | return 68 | } 69 | 70 | w.WriteHeader(http.StatusOK) 71 | _, err := w.Write(certData) 72 | if err != nil { 73 | t.Errorf("failed to write response: %s", err) 74 | } 75 | })) 76 | 77 | d := downloader{ 78 | certs: []CertificateAndPassphrase{{ 79 | URL: storage.URL, 80 | Passphrase: passphrase, 81 | }}, 82 | client: http.DefaultClient, 83 | } 84 | got, err := d.GetCertificates() 85 | 86 | want := []certificateutil.CertificateInfoModel{ 87 | certInfo, 88 | } 89 | 90 | assert.NoError(t, err) 91 | assert.Equal(t, want, got) 92 | } 93 | 94 | func createTestCert(t *testing.T) certificateutil.CertificateInfoModel { 95 | const ( 96 | teamID = "MYTEAMID" 97 | commonName = "Apple Developer: test" 98 | teamName = "BITFALL FEJLESZTO KORLATOLT FELELOSSEGU TARSASAG" 99 | ) 100 | expiry := time.Now().AddDate(1, 0, 0) 101 | serial := int64(1234) 102 | 103 | cert, privateKey, err := certificateutil.GenerateTestCertificate(serial, teamID, teamName, commonName, expiry) 104 | if err != nil { 105 | t.Errorf("init: failed to generate certificate: %s", err) 106 | } 107 | 108 | certInfo := certificateutil.NewCertificateInfo(*cert, privateKey) 109 | t.Logf("Test certificate generated. Serial: %s Team ID: %s Common name: %s", certInfo.Serial, certInfo.TeamID, certInfo.CommonName) 110 | 111 | return certInfo 112 | } 113 | -------------------------------------------------------------------------------- /autocodesign/entitlement_test.go: -------------------------------------------------------------------------------- 1 | package autocodesign 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestICloudContainers(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | projectEntitlements Entitlements 13 | want []string 14 | errHandler func(require.TestingT, error, ...interface{}) 15 | }{ 16 | { 17 | name: "no containers", 18 | projectEntitlements: Entitlements(map[string]interface{}{}), 19 | want: nil, 20 | errHandler: require.NoError, 21 | }, 22 | { 23 | name: "no containers - CloudDocuments", 24 | projectEntitlements: Entitlements(map[string]interface{}{ 25 | "com.apple.developer.icloud-services": []interface{}{ 26 | "CloudDocuments", 27 | }, 28 | }), 29 | want: nil, 30 | errHandler: require.NoError, 31 | }, 32 | { 33 | name: "no containers - CloudKit", 34 | projectEntitlements: Entitlements(map[string]interface{}{ 35 | "com.apple.developer.icloud-services": []interface{}{ 36 | "CloudKit", 37 | }, 38 | }), 39 | want: nil, 40 | errHandler: require.NoError, 41 | }, 42 | { 43 | name: "no containers - CloudKit and CloudDocuments", 44 | projectEntitlements: Entitlements(map[string]interface{}{ 45 | "com.apple.developer.icloud-services": []interface{}{ 46 | "CloudKit", 47 | "CloudDocuments", 48 | }, 49 | }), 50 | want: nil, 51 | errHandler: require.NoError, 52 | }, 53 | { 54 | name: "has containers - CloudDocuments", 55 | projectEntitlements: Entitlements(map[string]interface{}{ 56 | "com.apple.developer.icloud-services": []interface{}{ 57 | "CloudDocuments", 58 | }, 59 | "com.apple.developer.icloud-container-identifiers": []interface{}{ 60 | "iCloud.test.container.id", 61 | "iCloud.test.container.id2"}, 62 | }), 63 | want: []string{"iCloud.test.container.id", "iCloud.test.container.id2"}, 64 | errHandler: require.NoError, 65 | }, 66 | { 67 | name: "has containers - CloudKit", 68 | projectEntitlements: Entitlements(map[string]interface{}{ 69 | "com.apple.developer.icloud-services": []interface{}{ 70 | "CloudKit", 71 | }, 72 | "com.apple.developer.icloud-container-identifiers": []interface{}{ 73 | "iCloud.test.container.id", 74 | "iCloud.test.container.id2"}, 75 | }), 76 | want: []string{"iCloud.test.container.id", "iCloud.test.container.id2"}, 77 | errHandler: require.NoError, 78 | }, 79 | { 80 | name: "has containers - CloudKit and CloudDocuments", 81 | projectEntitlements: Entitlements(map[string]interface{}{ 82 | "com.apple.developer.icloud-services": []interface{}{ 83 | "CloudKit", 84 | "CloudDocuments", 85 | }, 86 | "com.apple.developer.icloud-container-identifiers": []interface{}{ 87 | "iCloud.test.container.id", 88 | "iCloud.test.container.id2"}, 89 | }), 90 | want: []string{"iCloud.test.container.id", "iCloud.test.container.id2"}, 91 | errHandler: require.NoError, 92 | }, 93 | } 94 | for _, tt := range tests { 95 | t.Run(tt.name, func(t *testing.T) { 96 | got, err := tt.projectEntitlements.ICloudContainers() 97 | require.Equal(t, got, tt.want) 98 | tt.errHandler(t, err) 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/appstoreconnect/tracker.go: -------------------------------------------------------------------------------- 1 | package appstoreconnect 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/bitrise-io/go-utils/v2/analytics" 7 | "github.com/bitrise-io/go-utils/v2/env" 8 | ) 9 | 10 | // Tracker defines the interface for tracking App Store Connect API usage and errors. 11 | type Tracker interface { 12 | // TrackAPIRequest tracks one HTTP request+response. This is called for each individual attempt in case of automatic retries. 13 | TrackAPIRequest(method, host, endpoint string, statusCode int, duration time.Duration, isRetry bool) 14 | 15 | // TrackAPIError tracks a failed API request with error details 16 | TrackAPIError(method, host, endpoint string, statusCode int, errorMessage string) 17 | 18 | // TrackAuthError tracks authentication-specific errors 19 | TrackAuthError(errorMessage string) 20 | } 21 | 22 | // NoOpAnalyticsTracker is a dummy implementation used in tests. 23 | type NoOpAnalyticsTracker struct{} 24 | 25 | // TrackAPIRequest ... 26 | func (n NoOpAnalyticsTracker) TrackAPIRequest(method, host, endpoint string, statusCode int, duration time.Duration, isRetry bool) { 27 | } 28 | 29 | // TrackAPIError ... 30 | func (n NoOpAnalyticsTracker) TrackAPIError(method, host, endpoint string, statusCode int, errorMessage string) { 31 | } 32 | 33 | // TrackAuthError ... 34 | func (n NoOpAnalyticsTracker) TrackAuthError(errorMessage string) {} 35 | 36 | // DefaultTracker is the main implementation of Tracker 37 | type DefaultTracker struct { 38 | tracker analytics.Tracker 39 | envRepo env.Repository 40 | } 41 | 42 | // NewDefaultTracker ... 43 | func NewDefaultTracker(tracker analytics.Tracker, envRepo env.Repository) *DefaultTracker { 44 | return &DefaultTracker{ 45 | tracker: tracker, 46 | envRepo: envRepo, 47 | } 48 | } 49 | 50 | // TrackAPIRequest ... 51 | func (d *DefaultTracker) TrackAPIRequest(method, host, endpoint string, statusCode int, duration time.Duration, isRetry bool) { 52 | d.tracker.Enqueue("step_appstoreconnect_request", analytics.Properties{ 53 | "build_slug": d.envRepo.Get("BITRISE_BUILD_SLUG"), 54 | "step_execution_id": d.envRepo.Get("BITRISE_STEP_EXECUTION_ID"), 55 | "http_method": method, 56 | "host": host, // Regular, enterprise, or any future third option 57 | "endpoint": endpoint, 58 | "status_code": statusCode, 59 | "duration_ms": duration.Truncate(time.Millisecond).Milliseconds(), 60 | "is_retry": isRetry, 61 | }) 62 | } 63 | 64 | // TrackAPIError ... 65 | func (d *DefaultTracker) TrackAPIError(method, host, endpoint string, statusCode int, errorMessage string) { 66 | d.tracker.Enqueue("step_appstoreconnect_error", analytics.Properties{ 67 | "build_slug": d.envRepo.Get("BITRISE_BUILD_SLUG"), 68 | "step_execution_id": d.envRepo.Get("BITRISE_STEP_EXECUTION_ID"), 69 | "http_method": method, 70 | "host": host, // Regular, enterprise, or any future third option 71 | "endpoint": endpoint, 72 | "status_code": statusCode, 73 | "error_message": errorMessage, 74 | }) 75 | } 76 | 77 | // TrackAuthError ... 78 | func (d *DefaultTracker) TrackAuthError(errorMessage string) { 79 | d.tracker.Enqueue("step_appstoreconnect_auth_error", analytics.Properties{ 80 | "build_slug": d.envRepo.Get("BITRISE_BUILD_SLUG"), 81 | "step_execution_id": d.envRepo.Get("BITRISE_STEP_EXECUTION_ID"), 82 | "error_message": errorMessage, 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /xcodecache/swiftpm_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/bitrise-io/go-steputils/cache" 10 | ) 11 | 12 | // SwiftPackagesStateInvalid is the partial error message printed out if swift packages cache is invalid. 13 | // Can be used to detect invalid state and clear the path returned by SwiftPackagesPath. 14 | // xcodebuild: error: Could not resolve package dependencies: 15 | // 16 | // The repository at [path] is invalid; try resetting package caches 17 | const SwiftPackagesStateInvalid = "Could not resolve package dependencies:" 18 | 19 | // SwiftPackageCache ... 20 | type SwiftPackageCache interface { 21 | SwiftPackagesPath(projectPth string) (string, error) 22 | CollectSwiftPackages(projectPath string) error 23 | } 24 | 25 | type swiftPackageCache struct { 26 | } 27 | 28 | // NewSwiftPackageCache ... 29 | func NewSwiftPackageCache() SwiftPackageCache { 30 | return &swiftPackageCache{} 31 | } 32 | 33 | // SwiftPackagesPath returns the Swift packages cache dir path. The input must be an absolute path. 34 | // The directory is: $HOME/Library/Developer/Xcode/DerivedData/[PER_PROJECT_DERIVED_DATA]/SourcePackages. 35 | func (c swiftPackageCache) SwiftPackagesPath(xcodeProjectPath string) (string, error) { 36 | if !path.IsAbs(xcodeProjectPath) { 37 | return "", fmt.Errorf("project path not an absolute path: %s", xcodeProjectPath) 38 | } 39 | 40 | fileExtension := filepath.Ext(xcodeProjectPath) 41 | if fileExtension != ".xcodeproj" && fileExtension != ".xcworkspace" && fileExtension != ".swift" { 42 | return "", fmt.Errorf("invalid Xcode project path: %s, no .xcodeproj or .xcworkspace suffix, or Package.swift file found", xcodeProjectPath) 43 | } 44 | 45 | trimmedXcodeProjectPath := strings.TrimSuffix(xcodeProjectPath, "/Package.swift") 46 | projectDerivedData, err := xcodeProjectDerivedDataPath(trimmedXcodeProjectPath) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | return path.Join(projectDerivedData, "SourcePackages"), nil 52 | } 53 | 54 | // CollectSwiftPackages marks the Swift Package Manager packages directory to be added the cache. 55 | // The directory cached is: $HOME/Library/Developer/Xcode/DerivedData/[PER_PROJECT_DERIVED_DATA]/SourcePackages. 56 | func (c swiftPackageCache) CollectSwiftPackages(xcodeProjectPath string) error { 57 | swiftPackagesDir, err := c.SwiftPackagesPath(xcodeProjectPath) 58 | if err != nil { 59 | return fmt.Errorf("failed to get Swift packages path, error %s", err) 60 | } 61 | 62 | cache := cache.New() 63 | cache.IncludePath(swiftPackagesDir) 64 | // Excluding manifest.db will result in a stable cache, as this file is modified in every build. 65 | cache.ExcludePath("!" + path.Join(swiftPackagesDir, "manifest.db")) 66 | 67 | if err := cache.Commit(); err != nil { 68 | return fmt.Errorf("failed to commit cache, error: %s", err) 69 | } 70 | return nil 71 | } 72 | 73 | // SwiftPackagesPath ... 74 | // Deprecated: SwiftPackagesPath is deprecated. Please use the SwiftPackageCache interface instead. 75 | func SwiftPackagesPath(xcodeProjectPath string) (string, error) { 76 | return NewSwiftPackageCache().SwiftPackagesPath(xcodeProjectPath) 77 | } 78 | 79 | // CollectSwiftPackages ... 80 | // Deprecated: CollectSwiftPackages is deprecated. Please use the SwiftPackageCache interface instead. 81 | func CollectSwiftPackages(xcodeProjectPath string) error { 82 | return NewSwiftPackageCache().CollectSwiftPackages(xcodeProjectPath) 83 | } 84 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/appstoreconnect/roundtripper_test.go: -------------------------------------------------------------------------------- 1 | package appstoreconnect 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | type attemptTracker struct { 14 | mu sync.Mutex 15 | attempts []attemptRecord 16 | } 17 | 18 | type attemptRecord struct { 19 | method string 20 | host string 21 | endpoint string 22 | statusCode int 23 | duration time.Duration 24 | isRetry bool 25 | } 26 | 27 | func (a *attemptTracker) TrackAPIRequest(method, host, endpoint string, statusCode int, duration time.Duration, isRetry bool) { 28 | a.mu.Lock() 29 | defer a.mu.Unlock() 30 | a.attempts = append(a.attempts, attemptRecord{ 31 | method: method, 32 | host: host, 33 | endpoint: endpoint, 34 | statusCode: statusCode, 35 | duration: duration, 36 | isRetry: isRetry, 37 | }) 38 | } 39 | 40 | func (a *attemptTracker) TrackAPIError(method, host, endpoint string, statusCode int, errorMessage string) { 41 | } 42 | 43 | func (a *attemptTracker) TrackAuthError(errorMessage string) { 44 | } 45 | 46 | func TestTrackingRoundTripper(t *testing.T) { 47 | t.Run("tracks single successful request", func(t *testing.T) { 48 | tracker := &attemptTracker{} 49 | 50 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 51 | w.WriteHeader(http.StatusOK) 52 | })) 53 | defer server.Close() 54 | 55 | transport := newTrackingRoundTripper(http.DefaultTransport, tracker) 56 | client := &http.Client{Transport: transport} 57 | 58 | req, err := http.NewRequest("GET", server.URL+"/test", nil) 59 | require.NoError(t, err) 60 | 61 | resp, err := client.Do(req) 62 | require.NoError(t, err) 63 | require.Equal(t, http.StatusOK, resp.StatusCode) 64 | 65 | tracker.mu.Lock() 66 | defer tracker.mu.Unlock() 67 | 68 | require.Len(t, tracker.attempts, 1) 69 | require.False(t, tracker.attempts[0].isRetry) 70 | require.Equal(t, http.StatusOK, tracker.attempts[0].statusCode) 71 | }) 72 | 73 | t.Run("tracks multiple attempts for same request", func(t *testing.T) { 74 | tracker := &attemptTracker{} 75 | attemptCount := 0 76 | 77 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 78 | attemptCount++ 79 | if attemptCount < 3 { 80 | w.WriteHeader(http.StatusTooManyRequests) 81 | } else { 82 | w.WriteHeader(http.StatusOK) 83 | } 84 | })) 85 | defer server.Close() 86 | 87 | retryableClient := NewRetryableHTTPClient(tracker) 88 | 89 | req, err := http.NewRequest("GET", server.URL+"/test", nil) 90 | require.NoError(t, err) 91 | 92 | resp, err := retryableClient.Do(req) 93 | require.NoError(t, err) 94 | require.Equal(t, http.StatusOK, resp.StatusCode) 95 | 96 | tracker.mu.Lock() 97 | defer tracker.mu.Unlock() 98 | 99 | require.Len(t, tracker.attempts, 3, "Expected 3 attempts to be tracked") 100 | 101 | require.False(t, tracker.attempts[0].isRetry) 102 | require.Equal(t, http.StatusTooManyRequests, tracker.attempts[0].statusCode) 103 | 104 | require.True(t, tracker.attempts[1].isRetry) 105 | require.Equal(t, http.StatusTooManyRequests, tracker.attempts[1].statusCode) 106 | 107 | require.True(t, tracker.attempts[2].isRetry) 108 | require.Equal(t, http.StatusOK, tracker.attempts[2].statusCode) 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /autocodesign/mock_Profile.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery 2.9.4. DO NOT EDIT. 2 | 3 | package autocodesign 4 | 5 | import ( 6 | appstoreconnect "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // MockProfile is an autogenerated mock type for the Profile type 11 | type MockProfile struct { 12 | mock.Mock 13 | } 14 | 15 | // Attributes provides a mock function with given fields: 16 | func (_m *MockProfile) Attributes() appstoreconnect.ProfileAttributes { 17 | ret := _m.Called() 18 | 19 | var r0 appstoreconnect.ProfileAttributes 20 | if rf, ok := ret.Get(0).(func() appstoreconnect.ProfileAttributes); ok { 21 | r0 = rf() 22 | } else { 23 | r0, ok = ret.Get(0).(appstoreconnect.ProfileAttributes) 24 | if !ok { 25 | } 26 | } 27 | 28 | return r0 29 | } 30 | 31 | // BundleID provides a mock function with given fields: 32 | func (_m *MockProfile) BundleID() (appstoreconnect.BundleID, error) { 33 | ret := _m.Called() 34 | 35 | var r0 appstoreconnect.BundleID 36 | if rf, ok := ret.Get(0).(func() appstoreconnect.BundleID); ok { 37 | r0 = rf() 38 | } else { 39 | r0, ok = ret.Get(0).(appstoreconnect.BundleID) 40 | if !ok { 41 | } 42 | } 43 | 44 | var r1 error 45 | if rf, ok := ret.Get(1).(func() error); ok { 46 | r1 = rf() 47 | } else { 48 | r1 = ret.Error(1) 49 | } 50 | 51 | return r0, r1 52 | } 53 | 54 | // CertificateIDs provides a mock function with given fields: 55 | func (_m *MockProfile) CertificateIDs() ([]string, error) { 56 | ret := _m.Called() 57 | 58 | var r0 []string 59 | if rf, ok := ret.Get(0).(func() []string); ok { 60 | r0 = rf() 61 | } else { 62 | if ret.Get(0) != nil { 63 | r0, ok = ret.Get(0).([]string) 64 | if !ok { 65 | } 66 | } 67 | } 68 | 69 | var r1 error 70 | if rf, ok := ret.Get(1).(func() error); ok { 71 | r1 = rf() 72 | } else { 73 | r1 = ret.Error(1) 74 | } 75 | 76 | return r0, r1 77 | } 78 | 79 | // DeviceUDIDs provides a mock function with given fields: 80 | func (_m *MockProfile) DeviceUDIDs() ([]string, error) { 81 | ret := _m.Called() 82 | 83 | var r0 []string 84 | if rf, ok := ret.Get(0).(func() []string); ok { 85 | r0 = rf() 86 | } else { 87 | if ret.Get(0) != nil { 88 | r0, ok = ret.Get(0).([]string) 89 | if !ok { 90 | } 91 | } 92 | } 93 | 94 | var r1 error 95 | if rf, ok := ret.Get(1).(func() error); ok { 96 | r1 = rf() 97 | } else { 98 | r1 = ret.Error(1) 99 | } 100 | 101 | return r0, r1 102 | } 103 | 104 | // Entitlements provides a mock function with given fields: 105 | func (_m *MockProfile) Entitlements() (Entitlements, error) { 106 | ret := _m.Called() 107 | 108 | var r0 Entitlements 109 | if rf, ok := ret.Get(0).(func() Entitlements); ok { 110 | r0 = rf() 111 | } else { 112 | if ret.Get(0) != nil { 113 | r0, ok = ret.Get(0).(Entitlements) 114 | if !ok { 115 | } 116 | } 117 | } 118 | 119 | var r1 error 120 | if rf, ok := ret.Get(1).(func() error); ok { 121 | r1 = rf() 122 | } else { 123 | r1 = ret.Error(1) 124 | } 125 | 126 | return r0, r1 127 | } 128 | 129 | // ID provides a mock function with given fields: 130 | func (_m *MockProfile) ID() string { 131 | ret := _m.Called() 132 | 133 | var r0 string 134 | if rf, ok := ret.Get(0).(func() string); ok { 135 | r0 = rf() 136 | } else { 137 | r0, ok = ret.Get(0).(string) 138 | if !ok { 139 | } 140 | } 141 | 142 | return r0 143 | } 144 | -------------------------------------------------------------------------------- /logio/prefix_filter_test.go: -------------------------------------------------------------------------------- 1 | //nolint:errcheck 2 | package logio_test 3 | 4 | import ( 5 | "io" 6 | "regexp" 7 | "testing" 8 | 9 | "github.com/bitrise-io/go-xcode/v2/logio" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | const ( 14 | msg1 = "Log message without prefix\n" 15 | msg2 = "[Bitrise Analytics] Log message with prefix\n" 16 | msg3 = "[Bitrise Build Cache] Log message with prefix\n" 17 | msg4 = "Stuff [Bitrise Build Cache] Log message without prefix\n" 18 | ) 19 | 20 | func TestPrefixInterceptor(t *testing.T) { 21 | re := regexp.MustCompile(`^\[Bitrise.*\].*`) 22 | matching := NewChanWriterCloser() 23 | matchingSink := logio.NewSink(matching) 24 | rest := NewChanWriterCloser() 25 | restSink := logio.NewSink(rest) 26 | 27 | sut := logio.NewPrefixFilter( 28 | re, 29 | matchingSink, 30 | restSink, 31 | ) 32 | 33 | _, _ = sut.Write([]byte(msg1)) 34 | _, _ = sut.Write([]byte(msg2)) 35 | _, _ = sut.Write([]byte(msg3)) 36 | _, _ = sut.Write([]byte(msg4)) 37 | 38 | _ = sut.Close() 39 | <-sut.Done() 40 | _ = matchingSink.Close() 41 | _ = restSink.Close() 42 | matching.Close() 43 | rest.Close() 44 | 45 | assert.Equal(t, msg2+msg3, matching.Messages()) 46 | assert.Equal(t, msg1+msg4, rest.Messages()) 47 | } 48 | 49 | func TestPrefixInterceptorWithPrematureClose(t *testing.T) { 50 | re := regexp.MustCompile(`^\[Bitrise.*\].*`) 51 | matching := NewChanWriterCloser() 52 | matchingSink := logio.NewSink(matching) 53 | rest := NewChanWriterCloser() 54 | restSink := logio.NewSink(rest) 55 | 56 | sut := logio.NewPrefixFilter( 57 | re, 58 | matchingSink, 59 | restSink, 60 | ) 61 | 62 | _, _ = sut.Write([]byte(msg1)) 63 | _, _ = sut.Write([]byte(msg2)) 64 | _, _ = sut.Write([]byte(msg3)) 65 | _ = sut.Close() 66 | _, _ = sut.Write([]byte(msg4)) 67 | 68 | <-sut.Done() 69 | 70 | _ = restSink.Close() 71 | _ = matchingSink.Close() 72 | matching.Close() 73 | rest.Close() 74 | 75 | assert.Equal(t, msg1, rest.Messages()) 76 | assert.Equal(t, msg2+msg3, matching.Messages()) 77 | } 78 | 79 | func TestPrefixInterceptorWithBlockedPipe(t *testing.T) { 80 | re := regexp.MustCompile(`^\[Bitrise.*\].*`) 81 | _, matching := io.Pipe() 82 | matchingSink := logio.NewSink(matching) 83 | rest := NewChanWriterCloser() 84 | restSink := logio.NewSink(rest) 85 | 86 | sut := logio.NewPrefixFilter( 87 | re, 88 | matchingSink, 89 | restSink, 90 | ) 91 | 92 | _, _ = sut.Write([]byte(msg1)) 93 | _, _ = sut.Write([]byte(msg2)) 94 | _, _ = sut.Write([]byte(msg3)) 95 | _ = sut.Close() 96 | _, _ = sut.Write([]byte(msg4)) 97 | 98 | <-sut.Done() 99 | 100 | _ = restSink.Close() 101 | matching.Close() 102 | rest.Close() 103 | 104 | assert.Equal(t, msg1, rest.Messages()) 105 | } 106 | 107 | // -------------------------------- 108 | // Helpers 109 | // -------------------------------- 110 | type ChanWriterCloser struct { 111 | channel chan string 112 | } 113 | 114 | func NewChanWriterCloser() *ChanWriterCloser { 115 | return &ChanWriterCloser{ 116 | channel: make(chan string, 1000), 117 | } 118 | } 119 | 120 | func (ch *ChanWriterCloser) Write(p []byte) (int, error) { 121 | ch.channel <- string(p) 122 | return len(p), nil 123 | } 124 | 125 | // Close stops the interceptor and closes the pipe. 126 | func (ch *ChanWriterCloser) Close() error { 127 | close(ch.channel) 128 | return nil 129 | } 130 | 131 | func (ch *ChanWriterCloser) Messages() string { 132 | var result string 133 | for msg := range ch.channel { 134 | result += msg 135 | } 136 | return result 137 | } 138 | -------------------------------------------------------------------------------- /xcarchive/utils.go: -------------------------------------------------------------------------------- 1 | package xcarchive 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/bitrise-io/go-utils/v2/command" 10 | "github.com/bitrise-io/go-xcode/v2/plistutil" 11 | ) 12 | 13 | func executableNameFromInfoPlist(infoPlist plistutil.PlistData) string { 14 | if name, ok := infoPlist.GetString("CFBundleExecutable"); ok { 15 | return name 16 | } 17 | return "" 18 | } 19 | 20 | func getEntitlements(cmdFactory command.Factory, basePath, executableRelativePath string) (plistutil.PlistData, error) { 21 | entitlements, err := entitlementsFromExecutable(cmdFactory, basePath, executableRelativePath) 22 | if err != nil { 23 | return plistutil.PlistData{}, err 24 | } 25 | 26 | if entitlements != nil { 27 | return *entitlements, nil 28 | } 29 | 30 | return plistutil.PlistData{}, nil 31 | } 32 | 33 | func entitlementsFromExecutable(cmdFactory command.Factory, basePath, executableRelativePath string) (*plistutil.PlistData, error) { 34 | fmt.Printf("Fetching entitlements from executable") 35 | 36 | cmd := cmdFactory.Create("codesign", []string{"--display", "--entitlements", ":-", filepath.Join(basePath, executableRelativePath)}, nil) 37 | entitlementsString, err := cmd.RunAndReturnTrimmedOutput() 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | plist, err := plistutil.NewPlistDataFromContent(entitlementsString) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return &plist, nil 48 | } 49 | 50 | func findDSYMs(archivePath string) ([]string, []string, error) { 51 | dsymsDirPth := filepath.Join(archivePath, "dSYMs") 52 | dsyms, err := listEntries(dsymsDirPth, extensionFilter(".dsym", true)) 53 | if err != nil { 54 | return []string{}, []string{}, err 55 | } 56 | 57 | appDSYMs := []string{} 58 | frameworkDSYMs := []string{} 59 | for _, dsym := range dsyms { 60 | if strings.HasSuffix(dsym, ".app.dSYM") { 61 | appDSYMs = append(appDSYMs, dsym) 62 | } else { 63 | frameworkDSYMs = append(frameworkDSYMs, dsym) 64 | } 65 | } 66 | 67 | return appDSYMs, frameworkDSYMs, nil 68 | } 69 | 70 | func escapeGlobPath(path string) string { 71 | var escaped string 72 | for _, ch := range path { 73 | if ch == '[' || ch == ']' || ch == '-' || ch == '*' || ch == '?' || ch == '\\' { 74 | escaped += "\\" 75 | } 76 | escaped += string(ch) 77 | } 78 | return escaped 79 | } 80 | 81 | type filterFunc func(string) (bool, error) 82 | 83 | func listEntries(dir string, filters ...filterFunc) ([]string, error) { 84 | absDir, err := filepath.Abs(dir) 85 | if err != nil { 86 | return []string{}, err 87 | } 88 | 89 | entries, err := os.ReadDir(absDir) 90 | if err != nil { 91 | return []string{}, err 92 | } 93 | 94 | var paths []string 95 | for _, entry := range entries { 96 | pth := filepath.Join(absDir, entry.Name()) 97 | paths = append(paths, pth) 98 | } 99 | 100 | return filterPaths(paths, filters...) 101 | } 102 | 103 | func filterPaths(fileList []string, filters ...filterFunc) ([]string, error) { 104 | var filtered []string 105 | 106 | for _, pth := range fileList { 107 | allowed := true 108 | for _, filter := range filters { 109 | if allows, err := filter(pth); err != nil { 110 | return []string{}, err 111 | } else if !allows { 112 | allowed = false 113 | break 114 | } 115 | } 116 | if allowed { 117 | filtered = append(filtered, pth) 118 | } 119 | } 120 | 121 | return filtered, nil 122 | } 123 | 124 | func extensionFilter(ext string, allowed bool) filterFunc { 125 | return func(pth string) (bool, error) { 126 | e := filepath.Ext(pth) 127 | return allowed == strings.EqualFold(ext, e), nil 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/spaceship/main.rb: -------------------------------------------------------------------------------- 1 | require_relative 'portal/auth_client' 2 | require_relative 'certificates' 3 | require_relative 'profiles' 4 | require_relative 'app' 5 | require_relative 'devices' 6 | require_relative 'log' 7 | require 'optparse' 8 | 9 | begin 10 | options = {} 11 | OptionParser.new do |opt| 12 | opt.on('--username USERNAME') { |o| options[:username] = o } 13 | opt.on('--password PASSWORD') { |o| options[:password] = o } 14 | opt.on('--session SESSION') { |o| options[:session] = Base64.decode64(o) } 15 | opt.on('--team-id TEAM_ID') { |o| options[:team_id] = o } 16 | opt.on('--subcommand SUBCOMMAND') { |o| options[:subcommand] = o } 17 | opt.on('--bundle-id BUNDLE_ID') { |o| options[:bundle_id] = o } 18 | opt.on('--bundle-id-name BUNDLE_ID_NAME') { |o| options[:bundle_id_name] = o } 19 | opt.on('--id ID') { |o| options[:id] = o } 20 | opt.on('--name NAME') { |o| options[:name] = o } 21 | opt.on('--certificate-id CERTIFICATE') { |o| options[:certificate_id] = o } 22 | opt.on('--profile-name PROFILE_NAME') { |o| options[:profile_name] = o } 23 | opt.on('--profile-type PROFILE_TYPE') { |o| options[:profile_type] = o } 24 | opt.on('--entitlements ENTITLEMENTS') { |o| options[:entitlements] = Base64.decode64(o) } 25 | opt.on('--udid UDID') { |o| options[:udid] = o } 26 | end.parse! 27 | 28 | FastlaneCore::Globals.verbose = true 29 | 30 | result = '{}' 31 | 32 | if options[:subcommand] == 'login' 33 | begin 34 | team_id = Portal::AuthClient.login(options[:username], options[:password], options[:session], options[:team_id]) 35 | result = team_id 36 | rescue => e 37 | puts "\nApple ID authentication failed: #{e}" 38 | exit(1) 39 | end 40 | else 41 | Portal::AuthClient.restore_from_session(options[:username], options[:team_id]) 42 | 43 | case options[:subcommand] 44 | when 'list_dev_certs' 45 | client = CertificateHelper.new 46 | result = client.list_dev_certs 47 | when 'list_dist_certs' 48 | client = CertificateHelper.new 49 | result = client.list_dist_certs 50 | when 'list_profiles' 51 | result = list_profiles(options[:profile_type], options[:profile_name]) 52 | when 'get_app' 53 | result = get_app(options[:bundle_id]) 54 | when 'create_app' 55 | result = create_app(options[:bundle_id], options[:bundle_id_name]) 56 | when 'delete_profile' 57 | delete_profile(options[:id]) 58 | result = { status: 'OK' } 59 | when 'create_profile' 60 | result = create_profile(options[:profile_type], options[:bundle_id], options[:certificate_id], options[:profile_name]) 61 | when 'check_bundleid' 62 | entitlements = JSON.parse(options[:entitlements]) 63 | check_bundleid(options[:bundle_id], entitlements) 64 | when 'sync_bundleid' 65 | entitlements = JSON.parse(options[:entitlements]) 66 | sync_bundleid(options[:bundle_id], entitlements) 67 | when 'list_devices' 68 | result = list_devices 69 | when 'register_device' 70 | result = register_device(options[:udid], options[:name]) 71 | else 72 | raise "Unknown subcommand: #{options[:subcommand]}" 73 | end 74 | end 75 | 76 | response = { data: result } 77 | puts response.to_json.to_s 78 | rescue RetryNeeded => e 79 | result = { retry: true, error: "#{e.cause}" } 80 | puts result.to_json.to_s 81 | rescue Spaceship::BasicPreferredInfoError, Spaceship::UnexpectedResponse => e 82 | result = { error: "#{e.preferred_error_info&.join("\n") || e.to_s}, stacktrace: #{e.backtrace.join("\n")}" } 83 | puts result.to_json.to_s 84 | rescue => e 85 | result = { error: "#{e}, stacktrace: #{e.backtrace.join("\n")}" } 86 | puts result.to_json.to_s 87 | end 88 | -------------------------------------------------------------------------------- /xcodecommand/xcbeautify.go: -------------------------------------------------------------------------------- 1 | package xcodecommand 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os/exec" 7 | "regexp" 8 | 9 | "github.com/bitrise-io/go-utils/v2/command" 10 | "github.com/bitrise-io/go-utils/v2/log" 11 | "github.com/bitrise-io/go-xcode/v2/errorfinder" 12 | "github.com/bitrise-io/go-xcode/v2/logio" 13 | version "github.com/hashicorp/go-version" 14 | ) 15 | 16 | const ( 17 | xcbeautify = "xcbeautify" 18 | ) 19 | 20 | // XcbeautifyRunner is a xcodebuild runner that uses xcbeautify as log formatter 21 | type XcbeautifyRunner struct { 22 | logger log.Logger 23 | commandFactory command.Factory 24 | } 25 | 26 | // NewXcbeautifyRunner returns a new xcbeautify runner 27 | func NewXcbeautifyRunner(logger log.Logger, commandFactory command.Factory) Runner { 28 | return &XcbeautifyRunner{ 29 | logger: logger, 30 | commandFactory: commandFactory, 31 | } 32 | } 33 | 34 | // Run runs xcodebuild using xcbeautify as an output formatter 35 | func (c *XcbeautifyRunner) Run(workDir string, xcodebuildArgs []string, xcbeautifyArgs []string) (Output, error) { 36 | loggingIO := logio.SetupPipeWiring(regexp.MustCompile(`^\[Bitrise.*\].*`)) 37 | 38 | // For parallel and concurrent destination testing, it helps to use unbuffered I/O for stdout and to redirect stderr to stdout. 39 | // NSUnbufferedIO=YES xcodebuild [args] 2>&1 | xcbeautify 40 | buildCmd := c.commandFactory.Create("xcodebuild", xcodebuildArgs, &command.Opts{ 41 | Stdout: loggingIO.XcbuildStdout, 42 | Stderr: loggingIO.XcbuildStderr, 43 | Env: unbufferedIOEnv, 44 | Dir: workDir, 45 | ErrorFinder: errorfinder.FindXcodebuildErrors, 46 | }) 47 | 48 | beautifyCmd := c.commandFactory.Create(xcbeautify, xcbeautifyArgs, &command.Opts{ 49 | Stdin: loggingIO.ToolStdin, 50 | Stdout: loggingIO.ToolStdout, 51 | Stderr: loggingIO.ToolStderr, 52 | Env: unbufferedIOEnv, 53 | }) 54 | 55 | defer func() { 56 | if err := loggingIO.Close(); err != nil { 57 | c.logger.Warnf("logging IO failure, error: %s", err) 58 | } 59 | 60 | if err := beautifyCmd.Wait(); err != nil { 61 | c.logger.Warnf("xcbeautify command failed: %s", err) 62 | } 63 | }() 64 | 65 | c.logger.TPrintf("$ set -o pipefail && %s | %s", buildCmd.PrintableCommandArgs(), beautifyCmd.PrintableCommandArgs()) 66 | 67 | err := buildCmd.Start() 68 | if err == nil { 69 | err = beautifyCmd.Start() 70 | } 71 | if err == nil { 72 | err = buildCmd.Wait() 73 | } 74 | 75 | exitCode := 0 76 | if err != nil { 77 | exitCode = -1 78 | 79 | var exerr *exec.ExitError 80 | if errors.As(err, &exerr) { 81 | exitCode = exerr.ExitCode() 82 | } 83 | } 84 | 85 | // Closing the filter to ensure all output is flushed and processed 86 | if err := loggingIO.CloseFilter(); err != nil { 87 | c.logger.Warnf("logging IO failure, error: %s", err) 88 | } 89 | 90 | return Output{ 91 | RawOut: loggingIO.XcbuildRawout.Bytes(), 92 | ExitCode: exitCode, 93 | }, err 94 | } 95 | 96 | // CheckInstall checks if xcbeautify is on the PATH and returns its version 97 | func (c *XcbeautifyRunner) CheckInstall() (*version.Version, error) { 98 | c.logger.Println() 99 | c.logger.Infof("Checking log formatter (xcbeautify) version") 100 | 101 | versionCmd := c.commandFactory.Create(xcbeautify, []string{"--version"}, nil) 102 | 103 | out, err := versionCmd.RunAndReturnTrimmedOutput() 104 | if err != nil { 105 | var exerr *exec.ExitError 106 | if errors.As(err, &exerr) { 107 | return nil, fmt.Errorf("xcbeautify version command failed: %w", err) 108 | } 109 | 110 | return nil, fmt.Errorf("failed to run xcbeautify command: %w", err) 111 | } 112 | 113 | return version.NewVersion(out) 114 | } 115 | -------------------------------------------------------------------------------- /mocks/Command.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Command is an autogenerated mock type for the Command type 8 | type Command struct { 9 | mock.Mock 10 | } 11 | 12 | // PrintableCommandArgs provides a mock function with given fields: 13 | func (_m *Command) PrintableCommandArgs() string { 14 | ret := _m.Called() 15 | 16 | var r0 string 17 | if rf, ok := ret.Get(0).(func() string); ok { 18 | r0 = rf() 19 | } else { 20 | r0 = ret.Get(0).(string) 21 | } 22 | 23 | return r0 24 | } 25 | 26 | // Run provides a mock function with given fields: 27 | func (_m *Command) Run() error { 28 | ret := _m.Called() 29 | 30 | var r0 error 31 | if rf, ok := ret.Get(0).(func() error); ok { 32 | r0 = rf() 33 | } else { 34 | r0 = ret.Error(0) 35 | } 36 | 37 | return r0 38 | } 39 | 40 | // RunAndReturnExitCode provides a mock function with given fields: 41 | func (_m *Command) RunAndReturnExitCode() (int, error) { 42 | ret := _m.Called() 43 | 44 | var r0 int 45 | var r1 error 46 | if rf, ok := ret.Get(0).(func() (int, error)); ok { 47 | return rf() 48 | } 49 | if rf, ok := ret.Get(0).(func() int); ok { 50 | r0 = rf() 51 | } else { 52 | r0 = ret.Get(0).(int) 53 | } 54 | 55 | if rf, ok := ret.Get(1).(func() error); ok { 56 | r1 = rf() 57 | } else { 58 | r1 = ret.Error(1) 59 | } 60 | 61 | return r0, r1 62 | } 63 | 64 | // RunAndReturnTrimmedCombinedOutput provides a mock function with given fields: 65 | func (_m *Command) RunAndReturnTrimmedCombinedOutput() (string, error) { 66 | ret := _m.Called() 67 | 68 | var r0 string 69 | var r1 error 70 | if rf, ok := ret.Get(0).(func() (string, error)); ok { 71 | return rf() 72 | } 73 | if rf, ok := ret.Get(0).(func() string); ok { 74 | r0 = rf() 75 | } else { 76 | r0 = ret.Get(0).(string) 77 | } 78 | 79 | if rf, ok := ret.Get(1).(func() error); ok { 80 | r1 = rf() 81 | } else { 82 | r1 = ret.Error(1) 83 | } 84 | 85 | return r0, r1 86 | } 87 | 88 | // RunAndReturnTrimmedOutput provides a mock function with given fields: 89 | func (_m *Command) RunAndReturnTrimmedOutput() (string, error) { 90 | ret := _m.Called() 91 | 92 | var r0 string 93 | var r1 error 94 | if rf, ok := ret.Get(0).(func() (string, error)); ok { 95 | return rf() 96 | } 97 | if rf, ok := ret.Get(0).(func() string); ok { 98 | r0 = rf() 99 | } else { 100 | r0 = ret.Get(0).(string) 101 | } 102 | 103 | if rf, ok := ret.Get(1).(func() error); ok { 104 | r1 = rf() 105 | } else { 106 | r1 = ret.Error(1) 107 | } 108 | 109 | return r0, r1 110 | } 111 | 112 | // Start provides a mock function with given fields: 113 | func (_m *Command) Start() error { 114 | ret := _m.Called() 115 | 116 | var r0 error 117 | if rf, ok := ret.Get(0).(func() error); ok { 118 | r0 = rf() 119 | } else { 120 | r0 = ret.Error(0) 121 | } 122 | 123 | return r0 124 | } 125 | 126 | // Wait provides a mock function with given fields: 127 | func (_m *Command) Wait() error { 128 | ret := _m.Called() 129 | 130 | var r0 error 131 | if rf, ok := ret.Get(0).(func() error); ok { 132 | r0 = rf() 133 | } else { 134 | r0 = ret.Error(0) 135 | } 136 | 137 | return r0 138 | } 139 | 140 | // NewCommand creates a new instance of Command. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 141 | // The first argument is typically a *testing.T value. 142 | func NewCommand(t interface { 143 | mock.TestingT 144 | Cleanup(func()) 145 | }) *Command { 146 | mock := &Command{} 147 | mock.Mock.Test(t) 148 | 149 | t.Cleanup(func() { mock.AssertExpectations(t) }) 150 | 151 | return mock 152 | } 153 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/certificates.go: -------------------------------------------------------------------------------- 1 | package spaceship 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "math/big" 8 | 9 | "github.com/bitrise-io/go-xcode/certificateutil" 10 | "github.com/bitrise-io/go-xcode/v2/autocodesign" 11 | "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" 12 | ) 13 | 14 | // CertificateSource ... 15 | type CertificateSource struct { 16 | client *Client 17 | certificates map[appstoreconnect.CertificateType][]autocodesign.Certificate 18 | } 19 | 20 | // NewSpaceshipCertificateSource ... 21 | func NewSpaceshipCertificateSource(client *Client) *CertificateSource { 22 | return &CertificateSource{ 23 | client: client, 24 | } 25 | } 26 | 27 | // QueryCertificateBySerial ... 28 | func (s *CertificateSource) QueryCertificateBySerial(serial big.Int) (autocodesign.Certificate, error) { 29 | if s.certificates == nil { 30 | if err := s.downloadAll(); err != nil { 31 | return autocodesign.Certificate{}, err 32 | } 33 | } 34 | 35 | allCerts := append(s.certificates[appstoreconnect.IOSDevelopment], s.certificates[appstoreconnect.IOSDistribution]...) 36 | for _, cert := range allCerts { 37 | if serial.Cmp(cert.CertificateInfo.Certificate.SerialNumber) == 0 { 38 | return cert, nil 39 | } 40 | } 41 | 42 | return autocodesign.Certificate{}, fmt.Errorf("can not find certificate with serial (%s)", serial.Text(16)) 43 | } 44 | 45 | // QueryAllIOSCertificates ... 46 | func (s *CertificateSource) QueryAllIOSCertificates() (map[appstoreconnect.CertificateType][]autocodesign.Certificate, error) { 47 | if s.certificates == nil { 48 | if err := s.downloadAll(); err != nil { 49 | return nil, err 50 | } 51 | } 52 | 53 | return s.certificates, nil 54 | } 55 | 56 | func (s *CertificateSource) downloadAll() error { 57 | fmt.Printf("Fetching developer certificates") 58 | 59 | devCerts, err := s.getCertificates(true) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | fmt.Printf("Fetching distribution certificates") 65 | 66 | distCers, err := s.getCertificates(false) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | s.certificates = map[appstoreconnect.CertificateType][]autocodesign.Certificate{ 72 | appstoreconnect.IOSDevelopment: devCerts, 73 | appstoreconnect.IOSDistribution: distCers, 74 | } 75 | 76 | return nil 77 | } 78 | 79 | type certificatesResponse struct { 80 | Data []struct { 81 | Content string `json:"content"` 82 | ID string `json:"id"` 83 | } `json:"data"` 84 | } 85 | 86 | func (s *CertificateSource) getCertificates(devCerts bool) ([]autocodesign.Certificate, error) { 87 | var output string 88 | var err error 89 | if devCerts { 90 | output, err = s.client.runSpaceshipCommand("list_dev_certs") 91 | } else { 92 | output, err = s.client.runSpaceshipCommand("list_dist_certs") 93 | } 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | var certificates certificatesResponse 99 | if err := json.Unmarshal([]byte(output), &certificates); err != nil { 100 | return nil, fmt.Errorf("failed to unmarshal response: %v", err) 101 | } 102 | 103 | var certInfos []autocodesign.Certificate 104 | for _, certInfo := range certificates.Data { 105 | pemContent, err := base64.StdEncoding.DecodeString(certInfo.Content) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | cert, err := certificateutil.CeritifcateFromPemContent(pemContent) 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | certInfos = append(certInfos, autocodesign.Certificate{ 116 | CertificateInfo: certificateutil.NewCertificateInfo(*cert, nil), 117 | ID: certInfo.ID, 118 | }) 119 | } 120 | 121 | return certInfos, nil 122 | } 123 | -------------------------------------------------------------------------------- /mocks/CommandFactory.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.20.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | command "github.com/bitrise-io/go-utils/v2/command" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // RubyCommandFactory is an autogenerated mock type for the CommandFactory type 11 | type RubyCommandFactory struct { 12 | mock.Mock 13 | } 14 | 15 | // Create provides a mock function with given fields: name, args, opts 16 | func (_m *RubyCommandFactory) Create(name string, args []string, opts *command.Opts) command.Command { 17 | ret := _m.Called(name, args, opts) 18 | 19 | var r0 command.Command 20 | if rf, ok := ret.Get(0).(func(string, []string, *command.Opts) command.Command); ok { 21 | r0 = rf(name, args, opts) 22 | } else { 23 | if ret.Get(0) != nil { 24 | r0 = ret.Get(0).(command.Command) 25 | } 26 | } 27 | 28 | return r0 29 | } 30 | 31 | // CreateBundleExec provides a mock function with given fields: name, args, bundlerVersion, opts 32 | func (_m *RubyCommandFactory) CreateBundleExec(name string, args []string, bundlerVersion string, opts *command.Opts) command.Command { 33 | ret := _m.Called(name, args, bundlerVersion, opts) 34 | 35 | var r0 command.Command 36 | if rf, ok := ret.Get(0).(func(string, []string, string, *command.Opts) command.Command); ok { 37 | r0 = rf(name, args, bundlerVersion, opts) 38 | } else { 39 | if ret.Get(0) != nil { 40 | r0 = ret.Get(0).(command.Command) 41 | } 42 | } 43 | 44 | return r0 45 | } 46 | 47 | // CreateBundleInstall provides a mock function with given fields: bundlerVersion, opts 48 | func (_m *RubyCommandFactory) CreateBundleInstall(bundlerVersion string, opts *command.Opts) command.Command { 49 | ret := _m.Called(bundlerVersion, opts) 50 | 51 | var r0 command.Command 52 | if rf, ok := ret.Get(0).(func(string, *command.Opts) command.Command); ok { 53 | r0 = rf(bundlerVersion, opts) 54 | } else { 55 | if ret.Get(0) != nil { 56 | r0 = ret.Get(0).(command.Command) 57 | } 58 | } 59 | 60 | return r0 61 | } 62 | 63 | // CreateGemInstall provides a mock function with given fields: gem, version, enablePrerelease, force, opts 64 | func (_m *RubyCommandFactory) CreateGemInstall(gem string, version string, enablePrerelease bool, force bool, opts *command.Opts) []command.Command { 65 | ret := _m.Called(gem, version, enablePrerelease, force, opts) 66 | 67 | var r0 []command.Command 68 | if rf, ok := ret.Get(0).(func(string, string, bool, bool, *command.Opts) []command.Command); ok { 69 | r0 = rf(gem, version, enablePrerelease, force, opts) 70 | } else { 71 | if ret.Get(0) != nil { 72 | r0 = ret.Get(0).([]command.Command) 73 | } 74 | } 75 | 76 | return r0 77 | } 78 | 79 | // CreateGemUpdate provides a mock function with given fields: gem, opts 80 | func (_m *RubyCommandFactory) CreateGemUpdate(gem string, opts *command.Opts) []command.Command { 81 | ret := _m.Called(gem, opts) 82 | 83 | var r0 []command.Command 84 | if rf, ok := ret.Get(0).(func(string, *command.Opts) []command.Command); ok { 85 | r0 = rf(gem, opts) 86 | } else { 87 | if ret.Get(0) != nil { 88 | r0 = ret.Get(0).([]command.Command) 89 | } 90 | } 91 | 92 | return r0 93 | } 94 | 95 | type mockConstructorTestingTNewRubyCommandFactory interface { 96 | mock.TestingT 97 | Cleanup(func()) 98 | } 99 | 100 | // NewRubyCommandFactory creates a new instance of RubyCommandFactory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 101 | func NewRubyCommandFactory(t mockConstructorTestingTNewRubyCommandFactory) *RubyCommandFactory { 102 | mock := &RubyCommandFactory{} 103 | mock.Mock.Test(t) 104 | 105 | t.Cleanup(func() { mock.AssertExpectations(t) }) 106 | 107 | return mock 108 | } 109 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/devices.go: -------------------------------------------------------------------------------- 1 | package spaceship 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/bitrise-io/go-utils/log" 10 | "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" 11 | "github.com/bitrise-io/go-xcode/v2/devportalservice" 12 | ) 13 | 14 | // DeviceClient ... 15 | type DeviceClient struct { 16 | client *Client 17 | } 18 | 19 | // NewDeviceClient ... 20 | func NewDeviceClient(client *Client) *DeviceClient { 21 | return &DeviceClient{client: client} 22 | } 23 | 24 | // DeviceInfo ... 25 | type DeviceInfo struct { 26 | ID string `json:"id"` 27 | UDID string `json:"udid"` 28 | Name string `json:"name"` 29 | Model string `json:"model"` 30 | Status appstoreconnect.Status `json:"status"` 31 | Platform appstoreconnect.BundleIDPlatform `json:"platform"` 32 | Class appstoreconnect.DeviceClass `json:"class"` 33 | } 34 | 35 | func newDevice(d DeviceInfo) appstoreconnect.Device { 36 | return appstoreconnect.Device{ 37 | ID: d.ID, 38 | Type: d.Model, 39 | Attributes: appstoreconnect.DeviceAttributes{ 40 | DeviceClass: d.Class, 41 | Model: d.Model, 42 | Name: d.Name, 43 | Platform: d.Platform, 44 | Status: d.Status, 45 | UDID: d.UDID, 46 | }, 47 | } 48 | } 49 | 50 | // ListDevices ... 51 | func (d *DeviceClient) ListDevices(udid string, platform appstoreconnect.DevicePlatform) ([]appstoreconnect.Device, error) { 52 | log.Debugf("Fetching devices") 53 | 54 | output, err := d.client.runSpaceshipCommand("list_devices") 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | var deviceResponse struct { 60 | Data []DeviceInfo `json:"data"` 61 | } 62 | if err := json.Unmarshal([]byte(output), &deviceResponse); err != nil { 63 | return nil, fmt.Errorf("failed to unmarshal response: %v", err) 64 | } 65 | 66 | var devices []appstoreconnect.Device 67 | for _, d := range deviceResponse.Data { 68 | devices = append(devices, newDevice(d)) 69 | } 70 | 71 | var filteredDevices []appstoreconnect.Device 72 | for _, d := range devices { 73 | if udid != "" && d.Attributes.UDID != udid { 74 | log.Debugf("Device filtered out, UDID required: %s actual: %s", udid, d.Attributes.UDID) 75 | continue 76 | } 77 | if d.Attributes.Platform != appstoreconnect.BundleIDPlatform(platform) { 78 | log.Debugf("Device filtered out, platform required: %s actual: %s", appstoreconnect.BundleIDPlatform(platform), d.Attributes.Platform) 79 | continue 80 | } 81 | 82 | filteredDevices = append(filteredDevices, d) 83 | } 84 | 85 | return filteredDevices, nil 86 | } 87 | 88 | // RegisterDevice ... 89 | func (d *DeviceClient) RegisterDevice(testDevice devportalservice.TestDevice) (*appstoreconnect.Device, error) { 90 | log.Debugf("Registering device") 91 | 92 | output, err := d.client.runSpaceshipCommand("register_device", 93 | "--udid", testDevice.DeviceID, 94 | "--name", testDevice.Title, 95 | ) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | var deviceResponse struct { 101 | Data struct { 102 | Device *DeviceInfo `json:"device"` 103 | Warnings []string `json:"warnings"` 104 | } `json:"data"` 105 | } 106 | if err := json.Unmarshal([]byte(output), &deviceResponse); err != nil { 107 | return nil, fmt.Errorf("failed to unmarshal response: %v", err) 108 | } 109 | 110 | if deviceResponse.Data.Device == nil { 111 | if len(deviceResponse.Data.Warnings) != 0 { 112 | return nil, appstoreconnect.DeviceRegistrationError{ 113 | Reason: strings.Join(deviceResponse.Data.Warnings, "\n"), 114 | } 115 | } 116 | 117 | return nil, errors.New("unexpected device registration failure") 118 | } 119 | 120 | device := newDevice(*deviceResponse.Data.Device) 121 | 122 | return &device, nil 123 | } 124 | -------------------------------------------------------------------------------- /_integration_tests/zip/ipa_reader_test.go: -------------------------------------------------------------------------------- 1 | package zip 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/bitrise-io/go-utils/v2/log" 9 | "github.com/bitrise-io/go-xcode/profileutil" 10 | "github.com/bitrise-io/go-xcode/v2/_integration_tests" 11 | "github.com/bitrise-io/go-xcode/v2/artifacts" 12 | internalzip "github.com/bitrise-io/go-xcode/v2/internal/zip" 13 | "github.com/bitrise-io/go-xcode/v2/plistutil" 14 | "github.com/bitrise-io/go-xcode/v2/zip" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestIPAReader_DefaultZipReader(t *testing.T) { 19 | sampleArtifactsDir := _integration_tests.GetSampleArtifactsRepository(t) 20 | watchTestIPAPath := filepath.Join(sampleArtifactsDir, "ipas", "watch-test.ipa") 21 | 22 | r, err := zip.NewDefaultReader(watchTestIPAPath, log.NewLogger()) 23 | require.NoError(t, err) 24 | defer func() { 25 | err := r.Close() 26 | require.NoError(t, err) 27 | }() 28 | 29 | plist, profile := readIPAWithStdlibZipReader(t, watchTestIPAPath) 30 | bundleID, _ := plist.GetString("CFBundleIdentifier") 31 | require.Equal(t, "bitrise.watch-test", bundleID) 32 | require.Equal(t, "XC iOS: *", profile.Name) 33 | } 34 | 35 | func TestIPAReader_StdlibZipReader(t *testing.T) { 36 | sampleArtifactsDir := _integration_tests.GetSampleArtifactsRepository(t) 37 | watchTestIPAPath := filepath.Join(sampleArtifactsDir, "ipas", "watch-test.ipa") 38 | 39 | plist, profile := readIPAWithStdlibZipReader(t, watchTestIPAPath) 40 | bundleID, _ := plist.GetString("CFBundleIdentifier") 41 | require.Equal(t, "bitrise.watch-test", bundleID) 42 | require.Equal(t, "XC iOS: *", profile.Name) 43 | } 44 | 45 | func TestIPAReader_DittoZipReader(t *testing.T) { 46 | sampleArtifactsDir := _integration_tests.GetSampleArtifactsRepository(t) 47 | watchTestIPAPath := filepath.Join(sampleArtifactsDir, "ipas", "watch-test.ipa") 48 | 49 | plist, profile := readIPAWithDittoZipReader(t, watchTestIPAPath) 50 | bundleID, _ := plist.GetString("CFBundleIdentifier") 51 | require.Equal(t, "bitrise.watch-test", bundleID) 52 | require.Equal(t, "XC iOS: *", profile.Name) 53 | } 54 | 55 | func Benchmark_ZipReaders(b *testing.B) { 56 | sampleArtifactsDir := _integration_tests.GetSampleArtifactsRepository(b) 57 | watchTestIPAPath := filepath.Join(sampleArtifactsDir, "ipas", "watch-test.ipa") 58 | 59 | for name, zipFunc := range map[string]readIPAFunc{ 60 | "dittoReader": func() (plistutil.PlistData, *profileutil.ProvisioningProfileInfoModel) { 61 | return readIPAWithDittoZipReader(b, watchTestIPAPath) 62 | }, 63 | "stdlibReader": func() (plistutil.PlistData, *profileutil.ProvisioningProfileInfoModel) { 64 | return readIPAWithStdlibZipReader(b, watchTestIPAPath) 65 | }, 66 | } { 67 | b.Run(fmt.Sprintf("Benchmarking %s", name), func(b *testing.B) { 68 | _, _ = zipFunc() 69 | }) 70 | } 71 | } 72 | 73 | type readIPAFunc func() (plistutil.PlistData, *profileutil.ProvisioningProfileInfoModel) 74 | 75 | func readIPAWithStdlibZipReader(t require.TestingT, archivePth string) (plistutil.PlistData, *profileutil.ProvisioningProfileInfoModel) { 76 | r, err := internalzip.NewStdlibRead(archivePth, log.NewLogger()) 77 | require.NoError(t, err) 78 | defer func() { 79 | err := r.Close() 80 | require.NoError(t, err) 81 | }() 82 | 83 | return readIPA(t, r) 84 | } 85 | 86 | func readIPAWithDittoZipReader(t require.TestingT, archivePth string) (plistutil.PlistData, *profileutil.ProvisioningProfileInfoModel) { 87 | r := internalzip.NewDittoReader(archivePth, log.NewLogger()) 88 | defer func() { 89 | err := r.Close() 90 | require.NoError(t, err) 91 | }() 92 | 93 | return readIPA(t, r) 94 | } 95 | 96 | func readIPA(t require.TestingT, zipReader artifacts.ZipReadCloser) (plistutil.PlistData, *profileutil.ProvisioningProfileInfoModel) { 97 | ipaReader := artifacts.NewIPAReader(zipReader) 98 | plist, err := ipaReader.AppInfoPlist() 99 | require.NoError(t, err) 100 | 101 | profile, err := ipaReader.ProvisioningProfileInfo() 102 | require.NoError(t, err) 103 | 104 | return plist, profile 105 | } 106 | -------------------------------------------------------------------------------- /autocodesign/utils_test.go: -------------------------------------------------------------------------------- 1 | package autocodesign 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/bitrise-io/go-xcode/certificateutil" 7 | "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" 8 | "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/time" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func Test_GivenCodeSignAssets_WhenMergingTwo_ThenValuesAreCorrect(t *testing.T) { 13 | dev1Profile := profile("base", "1") 14 | dev2Profile := profile("addition", "4") 15 | devUITest1Profile := profile("base", "2") 16 | devUITest2Profile := profile("addition-uitest", "5") 17 | enterprise1Profile := profile("enterprise", "1") 18 | adHoc1Profile := profile("ad-hoc", "1") 19 | 20 | certificate := certificateutil.CertificateInfoModel{} 21 | tests := []struct { 22 | name string 23 | base *AppCodesignAssets 24 | addition *AppCodesignAssets 25 | expected *AppCodesignAssets 26 | }{ 27 | { 28 | name: "Two existing assets with overlapping values", 29 | base: &AppCodesignAssets{ 30 | ArchivableTargetProfilesByBundleID: map[string]Profile{ 31 | "dev-1": dev1Profile, 32 | }, 33 | UITestTargetProfilesByBundleID: map[string]Profile{ 34 | "dev-uitest-1": devUITest1Profile, 35 | }, 36 | Certificate: certificate, 37 | }, 38 | addition: &AppCodesignAssets{ 39 | ArchivableTargetProfilesByBundleID: map[string]Profile{ 40 | "dev-2": dev2Profile, 41 | }, 42 | UITestTargetProfilesByBundleID: map[string]Profile{ 43 | "dev-uitest-2": devUITest2Profile, 44 | }, 45 | Certificate: certificate, 46 | }, 47 | expected: &AppCodesignAssets{ 48 | ArchivableTargetProfilesByBundleID: map[string]Profile{ 49 | "dev-1": dev1Profile, 50 | "dev-2": dev2Profile, 51 | }, 52 | UITestTargetProfilesByBundleID: map[string]Profile{ 53 | "dev-uitest-1": devUITest1Profile, 54 | "dev-uitest-2": devUITest2Profile, 55 | }, 56 | Certificate: certificate, 57 | }, 58 | }, 59 | { 60 | name: "Base value is empty", 61 | base: nil, 62 | addition: &AppCodesignAssets{ 63 | ArchivableTargetProfilesByBundleID: map[string]Profile{ 64 | "enterprise-1": enterprise1Profile, 65 | }, 66 | UITestTargetProfilesByBundleID: nil, 67 | Certificate: certificate, 68 | }, 69 | expected: &AppCodesignAssets{ 70 | ArchivableTargetProfilesByBundleID: map[string]Profile{ 71 | "enterprise-1": enterprise1Profile, 72 | }, 73 | UITestTargetProfilesByBundleID: nil, 74 | Certificate: certificate, 75 | }, 76 | }, 77 | { 78 | name: "Additional value is empty", 79 | base: &AppCodesignAssets{ 80 | ArchivableTargetProfilesByBundleID: map[string]Profile{ 81 | "ad-hoc-1": adHoc1Profile, 82 | }, 83 | UITestTargetProfilesByBundleID: nil, 84 | Certificate: certificate, 85 | }, 86 | addition: nil, 87 | expected: &AppCodesignAssets{ 88 | ArchivableTargetProfilesByBundleID: map[string]Profile{ 89 | "ad-hoc-1": adHoc1Profile, 90 | }, 91 | UITestTargetProfilesByBundleID: nil, 92 | Certificate: certificate, 93 | }, 94 | }, 95 | { 96 | name: "Empty values", 97 | base: nil, 98 | addition: nil, 99 | expected: nil, 100 | }, 101 | } 102 | 103 | for _, test := range tests { 104 | t.Run(test.name, func(t *testing.T) { 105 | merged := mergeCodeSignAssets(test.base, test.addition) 106 | assert.Equal(t, test.expected, merged) 107 | }) 108 | } 109 | } 110 | 111 | func profile(name, id string) Profile { 112 | return newMockProfile(profileArgs{ 113 | attributes: appstoreconnect.ProfileAttributes{ 114 | Name: name, 115 | UUID: id, 116 | ProfileContent: []byte{}, 117 | Platform: "", 118 | ExpirationDate: time.Time{}, 119 | }, 120 | id: id, 121 | appID: appstoreconnect.BundleID{}, 122 | devices: nil, 123 | certificates: nil, 124 | entitlements: Entitlements{}, 125 | }) 126 | } 127 | -------------------------------------------------------------------------------- /autocodesign/devportalclient/spaceship/spaceship/profiles.rb: -------------------------------------------------------------------------------- 1 | require 'spaceship' 2 | require_relative 'log' 3 | 4 | class Cert 5 | attr_accessor :id 6 | end 7 | 8 | class Profile 9 | attr_accessor :id 10 | end 11 | 12 | class RetryNeeded < StandardError; end 13 | 14 | def list_profiles(profile_type, name) 15 | profile_class = portal_profile_class(profile_type) 16 | sub_platform = portal_profile_sub_platform(profile_type) 17 | profiles = [] 18 | if sub_platform == 'tvOS' 19 | profiles = profile_class.all_tvos 20 | else 21 | profiles = profile_class.all(mac: false, xcode: false) 22 | end 23 | 24 | if name != '' 25 | matching_profiles = profiles.select { |prof| prof.name == name } 26 | end 27 | 28 | profile_infos = [] 29 | matching_profiles.each do |profile| 30 | profile_base64 = Base64.encode64(profile.download) 31 | 32 | profile_info = { 33 | id: profile.id, 34 | uuid: profile.uuid, 35 | name: profile.name, 36 | status: map_profile_status_to_api_status(profile.status), 37 | expiry: profile.expires, 38 | platform: map_profile_platform_to_api_platform(profile.platform), 39 | content: profile_base64, 40 | app_id: profile.app.app_id, 41 | bundle_id: profile.app.bundle_id, 42 | certificates: profile.certificates.map(&:id), 43 | devices: profile.devices.map(&:id) 44 | } 45 | profile_infos.append(profile_info) 46 | end 47 | 48 | profile_infos 49 | rescue => e 50 | raise e unless e.to_s =~ /Couldn't download provisioning profile/i 51 | 52 | raise RetryNeeded 53 | end 54 | 55 | def delete_profile(id) 56 | profile = Spaceship::Portal::ProvisioningProfile.new 57 | profile.id = id 58 | profile.delete! 59 | end 60 | 61 | def create_profile(profile_type, bundle_id, certificate_id, profile_name) 62 | cert = Cert.new 63 | cert.id = certificate_id 64 | 65 | profile_class = portal_profile_class(profile_type) 66 | sub_platform = portal_profile_sub_platform(profile_type) 67 | 68 | profile = profile_class.create!( 69 | name: profile_name, 70 | bundle_id: bundle_id, 71 | certificate: cert, 72 | sub_platform: sub_platform 73 | ) 74 | 75 | profile_base64 = Base64.encode64(profile.download) 76 | { 77 | id: profile.id, 78 | uuid: profile.uuid, 79 | name: profile.name, 80 | status: map_profile_platform_to_api_platform(profile.platform), 81 | expiry: profile.expires, 82 | platform: map_profile_platform_to_api_platform(profile.platform), 83 | content: profile_base64, 84 | app_id: profile.app.app_id, 85 | bundle_id: profile.app.bundle_id 86 | } 87 | rescue => e 88 | raise e unless e.to_s =~ /Multiple profiles found with the name/i || 89 | e.to_s =~ /Couldn't download provisioning profile/i 90 | 91 | raise RetryNeeded 92 | end 93 | 94 | def portal_profile_class(distribution_type) 95 | case distribution_type 96 | when 'IOS_APP_DEVELOPMENT', 'TVOS_APP_DEVELOPMENT' 97 | Spaceship::Portal.provisioning_profile.development 98 | when 'IOS_APP_STORE', 'TVOS_APP_STORE' 99 | Spaceship::Portal.provisioning_profile.app_store 100 | when 'IOS_APP_ADHOC', 'TVOS_APP_ADHOC' 101 | Spaceship::Portal.provisioning_profile.ad_hoc 102 | when 'IOS_APP_INHOUSE', 'TVOS_APP_INHOUSE' 103 | Spaceship::Portal.provisioning_profile.in_house 104 | else 105 | raise "invalid distribution type provided: #{distribution_type}" 106 | end 107 | end 108 | 109 | def portal_profile_sub_platform(distribution_type) 110 | %w[TVOS_APP_DEVELOPMENT TVOS_APP_DISTRIBUTION].include?(distribution_type) ? 'tvOS' : nil 111 | end 112 | 113 | def map_profile_status_to_api_status(status) 114 | case status 115 | when 'Active' 116 | 'ACTIVE' 117 | when 'Expired' 118 | 'EXPIRED' 119 | when 'Invalid' 120 | 'INVALID' 121 | else 122 | raise "invalid profile statuse #{status}" 123 | end 124 | end 125 | 126 | def map_profile_platform_to_api_platform(platform) 127 | case platform 128 | when 'ios' 129 | 'IOS' 130 | else 131 | raise "unsupported platform #{platform}" 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bitrise-io/go-xcode/v2 2 | 3 | go 1.22 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | cloud.google.com/go/secretmanager v1.14.3 9 | cloud.google.com/go/storage v1.50.0 10 | github.com/bitrise-io/go-plist v0.0.0-20210301100253-4b1a112ccd10 11 | github.com/bitrise-io/go-steputils v1.0.5 12 | github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.18 13 | github.com/bitrise-io/go-utils v1.0.12 14 | github.com/bitrise-io/go-utils/v2 v2.0.0-alpha.23 15 | github.com/bitrise-io/go-xcode v1.3.1 16 | github.com/globocom/go-buffer/v2 v2.0.0 17 | github.com/golang-jwt/jwt/v4 v4.5.0 18 | github.com/google/go-querystring v1.1.0 19 | github.com/hashicorp/go-retryablehttp v0.7.7 20 | github.com/hashicorp/go-version v1.6.0 21 | github.com/ryanuber/go-glob v1.0.0 22 | github.com/stretchr/testify v1.10.0 23 | golang.org/x/oauth2 v0.24.0 24 | golang.org/x/text v0.21.0 25 | google.golang.org/api v0.214.0 26 | ) 27 | 28 | require ( 29 | cel.dev/expr v0.16.1 // indirect 30 | cloud.google.com/go v0.116.0 // indirect 31 | cloud.google.com/go/auth v0.13.0 // indirect 32 | cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect 33 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 34 | cloud.google.com/go/iam v1.2.2 // indirect 35 | cloud.google.com/go/monitoring v1.21.2 // indirect 36 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect 37 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect 38 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect 39 | github.com/bitrise-io/go-pkcs12 v0.0.0-20230815095624-feb898696e02 // indirect 40 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 41 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect 42 | github.com/davecgh/go-spew v1.1.1 // indirect 43 | github.com/envoyproxy/go-control-plane/envoy v1.32.3 // indirect 44 | github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect 45 | github.com/felixge/httpsnoop v1.0.4 // indirect 46 | github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa // indirect 47 | github.com/go-logr/logr v1.4.2 // indirect 48 | github.com/go-logr/stdr v1.2.2 // indirect 49 | github.com/gofrs/uuid/v5 v5.2.0 // indirect 50 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 51 | github.com/google/s2a-go v0.1.8 // indirect 52 | github.com/google/uuid v1.6.0 // indirect 53 | github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect 54 | github.com/googleapis/gax-go/v2 v2.14.0 // indirect 55 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 56 | github.com/mitchellh/mapstructure v1.5.0 // indirect 57 | github.com/pkg/errors v0.9.1 // indirect 58 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 59 | github.com/pmezard/go-difflib v1.0.0 // indirect 60 | github.com/stretchr/objx v0.5.2 // indirect 61 | go.opencensus.io v0.24.0 // indirect 62 | go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect 63 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect 64 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 65 | go.opentelemetry.io/otel v1.29.0 // indirect 66 | go.opentelemetry.io/otel/metric v1.29.0 // indirect 67 | go.opentelemetry.io/otel/sdk v1.29.0 // indirect 68 | go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect 69 | go.opentelemetry.io/otel/trace v1.29.0 // indirect 70 | golang.org/x/crypto v0.31.0 // indirect 71 | golang.org/x/net v0.33.0 // indirect 72 | golang.org/x/sync v0.10.0 // indirect 73 | golang.org/x/sys v0.28.0 // indirect 74 | golang.org/x/term v0.27.0 // indirect 75 | golang.org/x/time v0.8.0 // indirect 76 | google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect 77 | google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect 78 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 79 | google.golang.org/grpc v1.67.3 // indirect 80 | google.golang.org/protobuf v1.35.2 // indirect 81 | gopkg.in/yaml.v3 v3.0.1 // indirect 82 | howett.net/plist v1.0.0 // indirect 83 | ) 84 | --------------------------------------------------------------------------------