From 11ab1b0f558a4386593601106dd095098bf6155d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krisztia=CC=81n=20Go=CC=88drei?= Date: Fri, 1 Aug 2025 10:52:05 +0200 Subject: [PATCH 1/6] Migrate xcarchive v1 --- .../archive_info_provider.go | 2 +- xcarchive/ios.go | 373 ++++++++++++++++- xcarchive/ios_test.go | 387 ++++++++++++++---- xcarchive/macos.go | 231 +++++++++++ xcarchive/macos_test.go | 78 ++++ xcarchive/utils.go | 68 +++ xcarchive/utils_test.go | 192 +++++++++ xcarchive/xcarchive.go | 43 ++ xcarchive/xcarchive_test.go | 45 ++ 9 files changed, 1340 insertions(+), 79 deletions(-) create mode 100644 xcarchive/macos.go create mode 100644 xcarchive/macos_test.go create mode 100644 xcarchive/utils.go create mode 100644 xcarchive/utils_test.go create mode 100644 xcarchive/xcarchive.go create mode 100644 xcarchive/xcarchive_test.go diff --git a/exportoptionsgenerator/archive_info_provider.go b/exportoptionsgenerator/archive_info_provider.go index 54b73f1f..416f2498 100644 --- a/exportoptionsgenerator/archive_info_provider.go +++ b/exportoptionsgenerator/archive_info_provider.go @@ -1,7 +1,7 @@ package exportoptionsgenerator import ( - "github.com/bitrise-io/go-xcode/xcarchive" + "github.com/bitrise-io/go-xcode/v2/xcarchive" ) // ExportProduct ... diff --git a/xcarchive/ios.go b/xcarchive/ios.go index b854c3c3..56108aa4 100644 --- a/xcarchive/ios.go +++ b/xcarchive/ios.go @@ -1,24 +1,296 @@ package xcarchive import ( + "errors" "fmt" + "path/filepath" + "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" - "github.com/bitrise-io/go-xcode/xcarchive" ) +// IosBaseApplication ... +type IosBaseApplication struct { + Path string + InfoPlist plistutil.PlistData + Entitlements plistutil.PlistData + ProvisioningProfile profileutil.ProvisioningProfileInfoModel +} + +// BundleIdentifier ... +func (app IosBaseApplication) BundleIdentifier() string { + bundleID, _ := app.InfoPlist.GetString("CFBundleIdentifier") + return bundleID +} + +// NewIosBaseApplication ... +func NewIosBaseApplication(path string) (IosBaseApplication, error) { + var infoPlist plistutil.PlistData + { + infoPlistPath := filepath.Join(path, "Info.plist") + if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { + return IosBaseApplication{}, fmt.Errorf("failed to check if Info.plist exists at: %s, error: %s", infoPlistPath, err) + } else if !exist { + return IosBaseApplication{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) + } + plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + if err != nil { + return IosBaseApplication{}, err + } + infoPlist = plist + } + + var provisioningProfile profileutil.ProvisioningProfileInfoModel + { + provisioningProfilePath := filepath.Join(path, "embedded.mobileprovision") + if exist, err := pathutil.IsPathExists(provisioningProfilePath); err != nil { + return IosBaseApplication{}, fmt.Errorf("failed to check if profile exists at: %s, error: %s", provisioningProfilePath, err) + } else if !exist { + return IosBaseApplication{}, fmt.Errorf("profile not exists at: %s", provisioningProfilePath) + } + + profile, err := profileutil.NewProvisioningProfileInfoFromFile(provisioningProfilePath) + if err != nil { + return IosBaseApplication{}, err + } + provisioningProfile = profile + } + + executable := executableNameFromInfoPlist(infoPlist) + entitlements, err := getEntitlements(path, executable) + if err != nil { + return IosBaseApplication{}, err + } + + return IosBaseApplication{ + Path: path, + InfoPlist: infoPlist, + Entitlements: entitlements, + ProvisioningProfile: provisioningProfile, + }, nil +} + +// IosExtension ... +type IosExtension struct { + IosBaseApplication +} + +// NewIosExtension ... +func NewIosExtension(path string) (IosExtension, error) { + baseApp, err := NewIosBaseApplication(path) + if err != nil { + return IosExtension{}, err + } + + return IosExtension{ + baseApp, + }, nil +} + +// IosWatchApplication ... +type IosWatchApplication struct { + IosBaseApplication + Extensions []IosExtension +} + +// IosClipApplication ... +type IosClipApplication struct { + IosBaseApplication +} + +// NewIosWatchApplication ... +func NewIosWatchApplication(path string) (IosWatchApplication, error) { + baseApp, err := NewIosBaseApplication(path) + if err != nil { + return IosWatchApplication{}, err + } + + extensions := []IosExtension{} + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "PlugIns/*.appex") + pths, err := filepath.Glob(pattern) + if err != nil { + return IosWatchApplication{}, fmt.Errorf("failed to search for watch application's extensions using pattern: %s, error: %s", pattern, err) + } + for _, pth := range pths { + extension, err := NewIosExtension(pth) + if err != nil { + return IosWatchApplication{}, err + } + + extensions = append(extensions, extension) + } + + return IosWatchApplication{ + IosBaseApplication: baseApp, + Extensions: extensions, + }, nil +} + +// NewIosClipApplication ... +func NewIosClipApplication(path string) (IosClipApplication, error) { + baseApp, err := NewIosBaseApplication(path) + if err != nil { + return IosClipApplication{}, err + } + + return IosClipApplication{ + IosBaseApplication: baseApp, + }, nil +} + +// IosApplication ... +type IosApplication struct { + IosBaseApplication + WatchApplication *IosWatchApplication + ClipApplication *IosClipApplication + Extensions []IosExtension +} + +// NewIosApplication ... +func NewIosApplication(path string) (IosApplication, error) { + baseApp, err := NewIosBaseApplication(path) + if err != nil { + return IosApplication{}, err + } + + var watchApp *IosWatchApplication + { + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "Watch/*.app") + pths, err := filepath.Glob(pattern) + if err != nil { + return IosApplication{}, err + } + if len(pths) > 0 { + watchPath := pths[0] + app, err := NewIosWatchApplication(watchPath) + if err != nil { + return IosApplication{}, err + } + watchApp = &app + } + } + + var clipApp *IosClipApplication + { + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "AppClips/*.app") + pths, err := filepath.Glob(pattern) + if err != nil { + return IosApplication{}, err + } + if len(pths) > 0 { + clipPath := pths[0] + app, err := NewIosClipApplication(clipPath) + if err != nil { + return IosApplication{}, err + } + clipApp = &app + } + } + + extensions := []IosExtension{} + { + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "PlugIns/*.appex") + pths, err := filepath.Glob(pattern) + if err != nil { + return IosApplication{}, fmt.Errorf("failed to search for watch application's extensions using pattern: %s, error: %s", pattern, err) + } + for _, pth := range pths { + extension, err := NewIosExtension(pth) + if err != nil { + return IosApplication{}, err + } + + extensions = append(extensions, extension) + } + } + + return IosApplication{ + IosBaseApplication: baseApp, + WatchApplication: watchApp, + ClipApplication: clipApp, + Extensions: extensions, + }, nil +} + // IosArchive ... type IosArchive struct { - xcarchive.IosArchive + Path string + InfoPlist plistutil.PlistData + Application IosApplication } // NewIosArchive ... func NewIosArchive(path string) (IosArchive, error) { - archive, err := xcarchive.NewIosArchive(path) + var infoPlist plistutil.PlistData + { + infoPlistPath := filepath.Join(path, "Info.plist") + if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { + return IosArchive{}, fmt.Errorf("failed to check if Info.plist exists at: %s, error: %s", infoPlistPath, err) + } else if !exist { + return IosArchive{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) + } + plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + if err != nil { + return IosArchive{}, err + } + infoPlist = plist + } + + var application IosApplication + { + appPath := "" + if appRelativePathToProducts, found := applicationFromPlist(infoPlist); found { + appPath = filepath.Join(path, "Products", appRelativePathToProducts) + } else { + var err error + if appPath, err = applicationFromArchive(path); err != nil { + return IosArchive{}, err + } + } + if exist, err := pathutil.IsPathExists(appPath); err != nil { + return IosArchive{}, fmt.Errorf("failed to check if app exists, path: %s, error: %s", appPath, err) + } else if !exist { + return IosArchive{}, fmt.Errorf("application not found on path: %s, error: %s", appPath, err) + } + + app, err := NewIosApplication(appPath) + if err != nil { + return IosArchive{}, err + } + application = app + } return IosArchive{ - IosArchive: archive, - }, err + Path: path, + InfoPlist: infoPlist, + Application: application, + }, nil +} + +func applicationFromPlist(InfoPlist plistutil.PlistData) (string, bool) { + if properties, found := InfoPlist.GetMapStringInterface("ApplicationProperties"); found { + return properties.GetString("ApplicationPath") + } + return "", false +} + +func applicationFromArchive(path string) (string, error) { + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "Products/Applications/*.app") + pths, err := filepath.Glob(pattern) + if err != nil { + return "", err + } + if len(pths) == 0 { + return "", fmt.Errorf("failed to find main app, using pattern: %s", pattern) + } + return pths[0], nil +} + +// IsXcodeManaged ... +func (archive IosArchive) IsXcodeManaged() bool { + return archive.Application.ProvisioningProfile.IsXcodeManaged() } // IsSigningManagedAutomatically ... @@ -26,6 +298,97 @@ func (archive IosArchive) IsSigningManagedAutomatically() (bool, error) { return archive.IsXcodeManaged(), nil } +// SigningIdentity ... +func (archive IosArchive) SigningIdentity() string { + if properties, found := archive.InfoPlist.GetMapStringInterface("ApplicationProperties"); found { + identity, _ := properties.GetString("SigningIdentity") + return identity + } + return "" +} + +// BundleIDEntitlementsMap ... +func (archive IosArchive) BundleIDEntitlementsMap() map[string]plistutil.PlistData { + bundleIDEntitlementsMap := map[string]plistutil.PlistData{} + + bundleID := archive.Application.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = archive.Application.Entitlements + + for _, plugin := range archive.Application.Extensions { + bundleID := plugin.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = plugin.Entitlements + } + + if archive.Application.WatchApplication != nil { + watchApplication := *archive.Application.WatchApplication + + bundleID := watchApplication.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = watchApplication.Entitlements + + for _, plugin := range watchApplication.Extensions { + bundleID := plugin.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = plugin.Entitlements + } + } + + if archive.Application.ClipApplication != nil { + clipApplication := *archive.Application.ClipApplication + + bundleID := clipApplication.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = clipApplication.Entitlements + } + + return bundleIDEntitlementsMap +} + +// BundleIDProfileInfoMap ... +func (archive IosArchive) BundleIDProfileInfoMap() map[string]profileutil.ProvisioningProfileInfoModel { + bundleIDProfileMap := map[string]profileutil.ProvisioningProfileInfoModel{} + + bundleID := archive.Application.BundleIdentifier() + bundleIDProfileMap[bundleID] = archive.Application.ProvisioningProfile + + for _, plugin := range archive.Application.Extensions { + bundleID := plugin.BundleIdentifier() + bundleIDProfileMap[bundleID] = plugin.ProvisioningProfile + } + + if archive.Application.WatchApplication != nil { + watchApplication := *archive.Application.WatchApplication + + bundleID := watchApplication.BundleIdentifier() + bundleIDProfileMap[bundleID] = watchApplication.ProvisioningProfile + + for _, plugin := range watchApplication.Extensions { + bundleID := plugin.BundleIdentifier() + bundleIDProfileMap[bundleID] = plugin.ProvisioningProfile + } + } + + if archive.Application.ClipApplication != nil { + clipApplication := *archive.Application.ClipApplication + + bundleID := clipApplication.BundleIdentifier() + bundleIDProfileMap[bundleID] = clipApplication.ProvisioningProfile + } + + return bundleIDProfileMap +} + +// FindDSYMs ... +func (archive IosArchive) FindDSYMs() ([]string, []string, error) { + return findDSYMs(archive.Path) +} + +// TeamID ... +func (archive IosArchive) TeamID() (string, error) { + bundleIDProfileInfoMap := archive.BundleIDProfileInfoMap() + for _, profileInfo := range bundleIDProfileInfoMap { + return profileInfo.TeamID, nil + } + return "", errors.New("team id not found") +} + // Platform ... func (archive IosArchive) Platform() (autocodesign.Platform, error) { platformName := archive.Application.InfoPlist["DTPlatformName"] diff --git a/xcarchive/ios_test.go b/xcarchive/ios_test.go index cbd28bcd..81d0d94c 100644 --- a/xcarchive/ios_test.go +++ b/xcarchive/ios_test.go @@ -1,14 +1,261 @@ package xcarchive import ( + "fmt" + "os" + "path/filepath" "reflect" "testing" + "github.com/bitrise-io/go-utils/command" + "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" - v1xcarchive "github.com/bitrise-io/go-xcode/xcarchive" + "github.com/stretchr/testify/require" ) +var tmpDir = "" + +func sampleRepoPath(t *testing.T) string { + dir := "" + if tmpDir != "" { + dir = tmpDir + } else { + var err error + dir, err = pathutil.NormalizedOSTempDirPath(tempDirName) + require.NoError(t, err) + sampleArtifactsGitURI := "https://github.com/bitrise-io/sample-artifacts.git" + cmd := command.New("git", "clone", sampleArtifactsGitURI, dir) + output, err := cmd.RunAndReturnTrimmedCombinedOutput() + if err != nil { + t.Log(output) + t.Errorf("git clone failed: %s", err) + } + tmpDir = dir + } + t.Logf("sample artifcats dir: %s\n", dir) + return dir +} + +func TestNewIosArchive(t *testing.T) { + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + require.Equal(t, 5, len(archive.InfoPlist)) + + app := archive.Application + require.Equal(t, 26, len(app.InfoPlist)) + require.Equal(t, 4, len(app.Entitlements)) + require.Equal(t, "*", app.ProvisioningProfile.BundleID) + + require.Equal(t, 1, len(app.Extensions)) + extension := app.Extensions[0] + require.Equal(t, 23, len(extension.InfoPlist)) + require.Equal(t, 4, len(extension.Entitlements)) + require.Equal(t, "*", extension.ProvisioningProfile.BundleID) + + require.NotNil(t, app.WatchApplication) + watchApp := *app.WatchApplication + require.Equal(t, 24, len(watchApp.InfoPlist)) + require.Equal(t, 4, len(watchApp.Entitlements)) + require.Equal(t, "*", watchApp.ProvisioningProfile.BundleID) + + require.Equal(t, 1, len(watchApp.Extensions)) + watchExtension := watchApp.Extensions[0] + require.Equal(t, 23, len(watchExtension.InfoPlist)) + require.Equal(t, 4, len(watchExtension.Entitlements)) + require.Equal(t, "*", watchExtension.ProvisioningProfile.BundleID) +} + +func TestNewAppClipArchive(t *testing.T) { + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/Fruta.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + require.Equal(t, 5, len(archive.InfoPlist)) + + app := archive.Application + require.Equal(t, 30, len(app.InfoPlist)) + require.Equal(t, 6, len(app.Entitlements)) + require.Equal(t, "io.bitrise.appcliptest", app.ProvisioningProfile.BundleID) + + require.Equal(t, 1, len(app.Extensions)) + extension := app.Extensions[0] + require.Equal(t, 24, len(extension.InfoPlist)) + require.Equal(t, 4, len(extension.Entitlements)) + require.Equal(t, "io.bitrise.appcliptest.ios-widgets", extension.ProvisioningProfile.BundleID) + + require.NotNil(t, app.ClipApplication) + clipApp := *app.ClipApplication + require.Equal(t, 31, len(clipApp.InfoPlist)) + require.Equal(t, 8, len(clipApp.Entitlements)) + require.Equal(t, "io.bitrise.appcliptest.Clip", clipApp.ProvisioningProfile.BundleID) +} + +func TestIsXcodeManaged(t *testing.T) { + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + + require.Equal(t, false, archive.IsXcodeManaged()) +} + +func TestSigningIdentity(t *testing.T) { + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + + require.Equal(t, "iPhone Developer: Bitrise Bot (VV2J4SV8V4)", archive.SigningIdentity()) +} + +func TestBundleIDEntitlementsMap(t *testing.T) { + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + + bundleIDEntitlementsMap := archive.BundleIDEntitlementsMap() + require.Equal(t, 4, len(bundleIDEntitlementsMap)) + + bundleIDs := []string{"com.bitrise.code-sign-test.share-extension", "com.bitrise.code-sign-test.watchkitapp", "com.bitrise.code-sign-test.watchkitapp.watchkitextension", "com.bitrise.code-sign-test"} + for _, bundleID := range bundleIDs { + _, ok := bundleIDEntitlementsMap[bundleID] + require.True(t, ok, fmt.Sprintf("%v", bundleIDEntitlementsMap)) + } +} + +func TestBundleIDProfileInfoMap(t *testing.T) { + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + + bundleIDProfileInfoMap := archive.BundleIDProfileInfoMap() + require.Equal(t, 4, len(bundleIDProfileInfoMap)) + + bundleIDs := []string{"com.bitrise.code-sign-test.share-extension", "com.bitrise.code-sign-test.watchkitapp", "com.bitrise.code-sign-test.watchkitapp.watchkitextension", "com.bitrise.code-sign-test"} + for _, bundleID := range bundleIDs { + _, ok := bundleIDProfileInfoMap[bundleID] + require.True(t, ok, fmt.Sprintf("%v", bundleIDProfileInfoMap)) + } +} + +func TestFindDSYMs(t *testing.T) { + // base case: dsyms for apps and frameworks + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/Fruta.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + + appDsym, otherDsyms, err := archive.FindDSYMs() + require.NoError(t, err) + require.Equal(t, 2, len(appDsym)) + require.Equal(t, 2, len(otherDsyms)) + + // no app dsym case: something has changed since the + // initial implementation of the function under test, + // and is causing dsyms with filenames to be generated + // even when dsym generation is turned off -- we don't care about + // other dsyms in this case, only whether the app dsym + // path is empty + noDSYMArchivePth := filepath.Join(sampleRepoPath(t), "archives/ios.ios-simple-objc.noappdsym.xcarchive") + archive, err = NewIosArchive(noDSYMArchivePth) + require.NoError(t, err) + + appDsym, _, err = archive.FindDSYMs() + require.NoError(t, err) + require.Empty(t, appDsym) +} + +func Test_applicationFromArchive(t *testing.T) { + var err error + tempDir, err := pathutil.NormalizedOSTempDirPath(t.Name()) + if err != nil { + t.Errorf("setup: failed to create temp dir") + } + archivePath := filepath.Join(tempDir, "{}GlobControlChars:a-b[ab]?*", "test.xcarchive") + appDir := filepath.Join(archivePath, "Products", "Applications") + appPath := filepath.Join(appDir, "test.app") + t.Logf("Test app path: %s", appPath) + err = os.MkdirAll(appDir, os.ModePerm) + if err != nil { + t.Errorf("setup: failed to create directory: %s, error: %s", appDir, err) + } + file, err := os.Create(appPath) + if err != nil { + t.Errorf("setup: failed to create test archive: %s, error: %s", appPath, err) + } + if err := file.Close(); err != nil { + t.Errorf("setup: failed to close file, error: %s", err) + } + + type args struct { + path string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "[] glob control characters in path", + args: args{ + path: archivePath, + }, + want: appPath, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := applicationFromArchive(tt.args.path) + if (err != nil) != tt.wantErr { + t.Errorf("applicationFromArchive() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("applicationFromArchive() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_applicationFromPlist(t *testing.T) { + infoPlist, err := plistutil.NewPlistDataFromFile(filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive/Info.plist")) + const appRelativePathToProduct = "Applications/code-sign-test.app" + if err != nil { + t.Errorf("setup: could not read plist, error: %s", infoPlist) + } + + type args struct { + InfoPlist plistutil.PlistData + } + tests := []struct { + name string + args args + want string + want1 bool + }{ + { + name: "normal case", + args: args{ + infoPlist, + }, + want: appRelativePathToProduct, + want1: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := applicationFromPlist(tt.args.InfoPlist) + if got != tt.want { + t.Errorf("applicationFromPlist() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("applicationFromPlist() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + func TestIosArchive_GetAppLayout(t *testing.T) { tests := []struct { name string @@ -19,16 +266,14 @@ func TestIosArchive_GetAppLayout(t *testing.T) { { name: "Single target app", archive: IosArchive{ - IosArchive: v1xcarchive.IosArchive{ - Application: v1xcarchive.IosApplication{ - IosBaseApplication: v1xcarchive.IosBaseApplication{ - InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.app", - "DTPlatformName": "iphoneos", - }, - ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ - TeamID: "1234ASDF", - }, + Application: IosApplication{ + IosBaseApplication: IosBaseApplication{ + InfoPlist: map[string]interface{}{ + "CFBundleIdentifier": "io.bitrise.app", + "DTPlatformName": "iphoneos", + }, + ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ + TeamID: "1234ASDF", }, }, }, @@ -43,45 +288,56 @@ func TestIosArchive_GetAppLayout(t *testing.T) { { name: "Multi target app", archive: IosArchive{ - IosArchive: v1xcarchive.IosArchive{ - Application: v1xcarchive.IosApplication{ - IosBaseApplication: v1xcarchive.IosBaseApplication{ + Application: IosApplication{ + IosBaseApplication: IosBaseApplication{ + InfoPlist: map[string]interface{}{ + "CFBundleIdentifier": "io.bitrise.app", + "DTPlatformName": "iphoneos", + }, + ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ + TeamID: "1234ASDF", + }, + }, + WatchApplication: &IosWatchApplication{ + IosBaseApplication: IosBaseApplication{ InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.app", - "DTPlatformName": "iphoneos", + "CFBundleIdentifier": "io.bitrise.watchapp", + "DTPlatformName": "watchos", }, ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ TeamID: "1234ASDF", }, }, - WatchApplication: &v1xcarchive.IosWatchApplication{ - IosBaseApplication: v1xcarchive.IosBaseApplication{ - InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.watchapp", - "DTPlatformName": "watchos", - }, - ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ - TeamID: "1234ASDF", - }, - }, - Extensions: []v1xcarchive.IosExtension{ - { - IosBaseApplication: v1xcarchive.IosBaseApplication{ - InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.watch-widget", - "DTPlatformName": "watchos", - }, - ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ - TeamID: "1234ASDF", - }, + Extensions: []IosExtension{ + { + IosBaseApplication: IosBaseApplication{ + InfoPlist: map[string]interface{}{ + "CFBundleIdentifier": "io.bitrise.watch-widget", + "DTPlatformName": "watchos", + }, + ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ + TeamID: "1234ASDF", }, }, }, }, - ClipApplication: &v1xcarchive.IosClipApplication{ - IosBaseApplication: v1xcarchive.IosBaseApplication{ + }, + ClipApplication: &IosClipApplication{ + IosBaseApplication: IosBaseApplication{ + InfoPlist: map[string]interface{}{ + "CFBundleIdentifier": "io.bitrise.clip", + "DTPlatformName": "iphoneos", + }, + ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ + TeamID: "1234ASDF", + }, + }, + }, + Extensions: []IosExtension{ + { + IosBaseApplication: IosBaseApplication{ InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.clip", + "CFBundleIdentifier": "io.bitrise.ios-widget1", "DTPlatformName": "iphoneos", }, ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ @@ -89,27 +345,14 @@ func TestIosArchive_GetAppLayout(t *testing.T) { }, }, }, - Extensions: []v1xcarchive.IosExtension{ - { - IosBaseApplication: v1xcarchive.IosBaseApplication{ - InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.ios-widget1", - "DTPlatformName": "iphoneos", - }, - ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ - TeamID: "1234ASDF", - }, + { + IosBaseApplication: IosBaseApplication{ + InfoPlist: map[string]interface{}{ + "CFBundleIdentifier": "io.bitrise.ios-widget2", + "DTPlatformName": "iphoneos", }, - }, - { - IosBaseApplication: v1xcarchive.IosBaseApplication{ - InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.ios-widget2", - "DTPlatformName": "iphoneos", - }, - ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ - TeamID: "1234ASDF", - }, + ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ + TeamID: "1234ASDF", }, }, }, @@ -131,20 +374,18 @@ func TestIosArchive_GetAppLayout(t *testing.T) { { name: "Single target app with capabilities", archive: IosArchive{ - IosArchive: v1xcarchive.IosArchive{ - Application: v1xcarchive.IosApplication{ - IosBaseApplication: v1xcarchive.IosBaseApplication{ - InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.app", - "DTPlatformName": "iphoneos", - }, - ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ - TeamID: "1234ASDF", - }, - Entitlements: map[string]interface{}{ - "get-task-allow": false, - "com.apple.security.application-groups": []string{"group.io.bitrise.app"}, - }, + Application: IosApplication{ + IosBaseApplication: IosBaseApplication{ + InfoPlist: map[string]interface{}{ + "CFBundleIdentifier": "io.bitrise.app", + "DTPlatformName": "iphoneos", + }, + ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ + TeamID: "1234ASDF", + }, + Entitlements: map[string]interface{}{ + "get-task-allow": false, + "com.apple.security.application-groups": []string{"group.io.bitrise.app"}, }, }, }, diff --git a/xcarchive/macos.go b/xcarchive/macos.go new file mode 100644 index 00000000..929a6ce4 --- /dev/null +++ b/xcarchive/macos.go @@ -0,0 +1,231 @@ +package xcarchive + +import ( + "fmt" + "path/filepath" + + "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/profileutil" + + "github.com/bitrise-io/go-utils/pathutil" +) + +type macosBaseApplication struct { + Path string + InfoPlist plistutil.PlistData + Entitlements plistutil.PlistData + ProvisioningProfile *profileutil.ProvisioningProfileInfoModel +} + +// BundleIdentifier ... +func (app macosBaseApplication) BundleIdentifier() string { + bundleID, _ := app.InfoPlist.GetString("CFBundleIdentifier") + return bundleID +} + +func newMacosBaseApplication(path string) (macosBaseApplication, error) { + var infoPlist plistutil.PlistData + { + infoPlistPath := filepath.Join(path, "Contents/Info.plist") + if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { + return macosBaseApplication{}, fmt.Errorf("failed to check if Info.plist exists at: %s, error: %s", infoPlistPath, err) + } else if !exist { + return macosBaseApplication{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) + } + plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + if err != nil { + return macosBaseApplication{}, err + } + infoPlist = plist + } + + var provisioningProfile *profileutil.ProvisioningProfileInfoModel + { + provisioningProfilePath := filepath.Join(path, "Contents/embedded.provisionprofile") + if exist, err := pathutil.IsPathExists(provisioningProfilePath); err != nil { + return macosBaseApplication{}, fmt.Errorf("failed to check if profile exists at: %s, error: %s", provisioningProfilePath, err) + } else if exist { + profile, err := profileutil.NewProvisioningProfileInfoFromFile(provisioningProfilePath) + if err != nil { + return macosBaseApplication{}, err + } + provisioningProfile = &profile + } + } + + executable := filepath.Join("/Contents/MacOS/", executableNameFromInfoPlist(infoPlist)) + entitlements, err := getEntitlements(path, executable) + if err != nil { + return macosBaseApplication{}, err + } + + return macosBaseApplication{ + Path: path, + InfoPlist: infoPlist, + Entitlements: entitlements, + ProvisioningProfile: provisioningProfile, + }, nil +} + +// MacosExtension ... +type MacosExtension struct { + macosBaseApplication +} + +// NewMacosExtension ... +func NewMacosExtension(path string) (MacosExtension, error) { + baseApp, err := newMacosBaseApplication(path) + if err != nil { + return MacosExtension{}, err + } + + return MacosExtension{ + baseApp, + }, nil +} + +// MacosApplication ... +type MacosApplication struct { + macosBaseApplication + Extensions []MacosExtension +} + +// NewMacosApplication ... +func NewMacosApplication(path string) (MacosApplication, error) { + baseApp, err := newMacosBaseApplication(path) + if err != nil { + return MacosApplication{}, err + } + + extensions := []MacosExtension{} + { + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "Contents/PlugIns/*.appex") + pths, err := filepath.Glob(pattern) + if err != nil { + return MacosApplication{}, fmt.Errorf("failed to search for watch application's extensions using pattern: %s, error: %s", pattern, err) + } + for _, pth := range pths { + extension, err := NewMacosExtension(pth) + if err != nil { + return MacosApplication{}, err + } + + extensions = append(extensions, extension) + } + } + + return MacosApplication{ + macosBaseApplication: baseApp, + Extensions: extensions, + }, nil +} + +// MacosArchive ... +type MacosArchive struct { + Path string + InfoPlist plistutil.PlistData + Application MacosApplication +} + +// NewMacosArchive ... +func NewMacosArchive(path string) (MacosArchive, error) { + var infoPlist plistutil.PlistData + { + infoPlistPath := filepath.Join(path, "Info.plist") + if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { + return MacosArchive{}, fmt.Errorf("failed to check if Info.plist exists at: %s, error: %s", infoPlistPath, err) + } else if !exist { + return MacosArchive{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) + } + plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + if err != nil { + return MacosArchive{}, err + } + infoPlist = plist + } + + var application MacosApplication + { + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "Products/Applications/*.app") + pths, err := filepath.Glob(pattern) + if err != nil { + return MacosArchive{}, err + } + + appPath := "" + if len(pths) > 0 { + appPath = pths[0] + } else { + return MacosArchive{}, fmt.Errorf("failed to find main app, using pattern: %s", pattern) + } + + app, err := NewMacosApplication(appPath) + if err != nil { + return MacosArchive{}, err + } + application = app + } + + return MacosArchive{ + Path: path, + InfoPlist: infoPlist, + Application: application, + }, nil +} + +// IsXcodeManaged ... +func (archive MacosArchive) IsXcodeManaged() bool { + if archive.Application.ProvisioningProfile != nil { + return archive.Application.ProvisioningProfile.IsXcodeManaged() + } + return false +} + +// SigningIdentity ... +func (archive MacosArchive) SigningIdentity() string { + properties, found := archive.InfoPlist.GetMapStringInterface("ApplicationProperties") + if found { + identity, _ := properties.GetString("SigningIdentity") + return identity + } + return "" +} + +// BundleIDEntitlementsMap ... +func (archive MacosArchive) BundleIDEntitlementsMap() map[string]plistutil.PlistData { + bundleIDEntitlementsMap := map[string]plistutil.PlistData{} + + bundleID := archive.Application.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = archive.Application.Entitlements + + for _, plugin := range archive.Application.Extensions { + bundleID := plugin.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = plugin.Entitlements + } + + return bundleIDEntitlementsMap +} + +// BundleIDProfileInfoMap ... +func (archive MacosArchive) BundleIDProfileInfoMap() map[string]profileutil.ProvisioningProfileInfoModel { + bundleIDProfileMap := map[string]profileutil.ProvisioningProfileInfoModel{} + + if archive.Application.ProvisioningProfile != nil { + bundleID := archive.Application.BundleIdentifier() + bundleIDProfileMap[bundleID] = *archive.Application.ProvisioningProfile + } + + for _, plugin := range archive.Application.Extensions { + if plugin.ProvisioningProfile != nil { + bundleID := plugin.BundleIdentifier() + bundleIDProfileMap[bundleID] = *plugin.ProvisioningProfile + } + } + + return bundleIDProfileMap +} + +// FindDSYMs ... +func (archive MacosArchive) FindDSYMs() ([]string, []string, error) { + return findDSYMs(archive.Path) +} diff --git a/xcarchive/macos_test.go b/xcarchive/macos_test.go new file mode 100644 index 00000000..335af258 --- /dev/null +++ b/xcarchive/macos_test.go @@ -0,0 +1,78 @@ +package xcarchive + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewMacosArchive(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + require.Equal(t, 5, len(archive.InfoPlist)) + + app := archive.Application + require.Equal(t, 21, len(app.InfoPlist)) + require.Equal(t, 2, len(app.Entitlements)) + require.Nil(t, app.ProvisioningProfile) + + require.Equal(t, 1, len(app.Extensions)) + extension := app.Extensions[0] + require.Equal(t, 22, len(extension.InfoPlist)) + require.Equal(t, 2, len(extension.Entitlements)) + require.Nil(t, extension.ProvisioningProfile) +} + +func TestMacosIsXcodeManaged(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + require.Equal(t, false, archive.IsXcodeManaged()) +} + +func TestMacosSigningIdentity(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + require.Equal(t, "Mac Developer: Gödrei Krisztian (T3694PR6UJ)", archive.SigningIdentity()) +} + +func TestMacosBundleIDEntitlementsMap(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + bundleIDEntitlementsMap := archive.BundleIDEntitlementsMap() + require.Equal(t, 2, len(bundleIDEntitlementsMap)) + + bundleIDs := []string{"io.bitrise.archive.Test", "io.bitrise.archive.Test.ActionExtension"} + for _, bundleID := range bundleIDs { + _, ok := bundleIDEntitlementsMap[bundleID] + require.True(t, ok, fmt.Sprintf("%v", bundleIDEntitlementsMap)) + } +} + +func TestMacosBundleIDProfileInfoMap(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + bundleIDProfileInfoMap := archive.BundleIDProfileInfoMap() + require.Equal(t, 0, len(bundleIDProfileInfoMap)) +} + +func TestMacosFindDSYMs(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + appDsym, otherDsyms, err := archive.FindDSYMs() + require.NoError(t, err) + require.Equal(t, 1, len(appDsym)) + require.Equal(t, 1, len(otherDsyms)) +} diff --git a/xcarchive/utils.go b/xcarchive/utils.go new file mode 100644 index 00000000..987257c7 --- /dev/null +++ b/xcarchive/utils.go @@ -0,0 +1,68 @@ +package xcarchive + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/bitrise-io/go-utils/command" + "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-xcode/plistutil" +) + +func executableNameFromInfoPlist(infoPlist plistutil.PlistData) string { + if name, ok := infoPlist.GetString("CFBundleExecutable"); ok { + return name + } + return "" +} + +func getEntitlements(basePath, executableRelativePath string) (plistutil.PlistData, error) { + entitlements, err := entitlementsFromExecutable(basePath, executableRelativePath) + if err != nil { + return plistutil.PlistData{}, err + } + + if entitlements != nil { + return *entitlements, nil + } + + return plistutil.PlistData{}, nil +} + +func entitlementsFromExecutable(basePath, executableRelativePath string) (*plistutil.PlistData, error) { + fmt.Printf("Fetching entitlements from executable") + + cmd := command.New("codesign", "--display", "--entitlements", ":-", filepath.Join(basePath, executableRelativePath)) + entitlementsString, err := cmd.RunAndReturnTrimmedOutput() + if err != nil { + return nil, err + } + + plist, err := plistutil.NewPlistDataFromContent(entitlementsString) + if err != nil { + return nil, err + } + + return &plist, nil +} + +func findDSYMs(archivePath string) ([]string, []string, error) { + dsymsDirPth := filepath.Join(archivePath, "dSYMs") + dsyms, err := pathutil.ListEntries(dsymsDirPth, pathutil.ExtensionFilter(".dsym", true)) + if err != nil { + return []string{}, []string{}, err + } + + appDSYMs := []string{} + frameworkDSYMs := []string{} + for _, dsym := range dsyms { + if strings.HasSuffix(dsym, ".app.dSYM") { + appDSYMs = append(appDSYMs, dsym) + } else { + frameworkDSYMs = append(frameworkDSYMs, dsym) + } + } + + return appDSYMs, frameworkDSYMs, nil +} diff --git a/xcarchive/utils_test.go b/xcarchive/utils_test.go new file mode 100644 index 00000000..aeca2047 --- /dev/null +++ b/xcarchive/utils_test.go @@ -0,0 +1,192 @@ +package xcarchive + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/bitrise-io/go-utils/pathutil" + + "github.com/bitrise-io/go-xcode/plistutil" + "github.com/stretchr/testify/assert" +) + +func TestGiveniOS_WhenAskingForEntitlements_ThenReadsItFromTheExecutable(t *testing.T) { + // Given + appPath := filepath.Join(sampleRepoPath(t), "archives/Fruta.xcarchive/Products/Applications/Fruta.app") + executable := executableRelativePath(appPath, "Info.plist", "") + + // When + entitlements, err := getEntitlements(appPath, executable) + + // Then + assert.NoError(t, err) + assert.Equal(t, iosEntitlements(), entitlements) +} + +func TestGivenMacos_WhenAskingForEntitlements_ThenReadsItFromTheExecutable(t *testing.T) { + // Given + appPath := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive/Products/Applications/Test.app") + executable := executableRelativePath(appPath, "Contents/Info.plist", "Contents/MacOS/") + + // When + entitlements, err := getEntitlements(appPath, executable) + + // Then + assert.NoError(t, err) + assert.Equal(t, macosEntitlements(), entitlements) +} + +func executableRelativePath(basePath, infoPlistRelativePath, executableFolderRelativePath string) string { + infoPlistPath := filepath.Join(basePath, infoPlistRelativePath) + exist, err := pathutil.IsPathExists(infoPlistPath) + if err != nil { + return "" + } + + if exist == false { + return "" + } + + plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + if err != nil { + return "" + } + + return filepath.Join(executableFolderRelativePath, executableNameFromInfoPlist(plist)) +} + +func iosEntitlements() plistutil.PlistData { + return map[string]interface{}{ + "application-identifier": "72SA8V3WYL.io.bitrise.appcliptest", + "com.apple.developer.applesignin": []interface{}{"Default"}, + "com.apple.developer.icloud-container-identifiers": []interface{}{}, + "com.apple.developer.team-identifier": "72SA8V3WYL", + "com.apple.security.application-groups": []interface{}{"group.io.bitrise.appcliptest"}, + "get-task-allow": false, + } +} + +func macosEntitlements() plistutil.PlistData { + return map[string]interface{}{ + "com.apple.security.app-sandbox": true, + "com.apple.security.files.user-selected.read-only": true, + } +} + +func Test_GivenArchiveWithMultipleAppAndFrameworkDSYMs_WhenFindDSYMsCalled_ThenExpectAllDSYMsToBeReturned(t *testing.T) { + testCases := []struct { + name string + numberOfAppDSYMs int + numberOfFrameworkDSYMs int + }{ + { + name: "1. Given archive with multiple app and framework dSYMs when FindDSYMs called then expect all dSYMs to be returned", + numberOfAppDSYMs: 2, + numberOfFrameworkDSYMs: 2, + }, + { + name: "2. Given archive with singe app and framework dSYMs when FindDSYMs called then expect both dSYMs to be returned", + numberOfAppDSYMs: 1, + numberOfFrameworkDSYMs: 1, + }, + { + name: "3. Given archive with multiple app dSYMs when FindDSYMs called then expect all app dSYMs to be returned", + numberOfAppDSYMs: 2, + numberOfFrameworkDSYMs: 0, + }, + { + name: "4. Given archive with multiple framework dSYMs when FindDSYMs called then expect all framework dSYMs to be returned", + numberOfAppDSYMs: 0, + numberOfFrameworkDSYMs: 2, + }, + { + name: "5. Given archive with single app dSYM when FindDSYMs called then expect the app dSYM to be returned", + numberOfAppDSYMs: 1, + numberOfFrameworkDSYMs: 0, + }, + { + name: "6. Given archive with single framework dSYM when FindDSYMs called then expect the framework dSYM to be returned", + numberOfAppDSYMs: 0, + numberOfFrameworkDSYMs: 1, + }, + { + name: "7. Given archive without any dSYM when FindDSYMs called then expect no dSYM to be returned", + numberOfAppDSYMs: 0, + numberOfFrameworkDSYMs: 0, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + archivePath, err := createArchiveWithAppAndFrameworkDSYMs( + "archives/ios.dsyms.xcarchive", + testCase.numberOfAppDSYMs, + testCase.numberOfFrameworkDSYMs, + ) + assert.NoError(t, err) + + appDSYMs, frameworkDSYMs, err := findDSYMs(archivePath) + assert.NoError(t, err) + assert.Equal(t, testCase.numberOfAppDSYMs, len(appDSYMs)) + assert.Equal(t, testCase.numberOfFrameworkDSYMs, len(frameworkDSYMs)) + }) + } +} + +func createArchiveWithAppAndFrameworkDSYMs(archivePath string, numberOfAppDSYMs, numberOfFrameworkDSYMs int) (string, error) { + archivePath, err := createArchive(archivePath) + if err != nil { + return "", err + } + + err = createAppDSYMs(archivePath, numberOfAppDSYMs) + if err != nil { + return "", err + } + + err = createFrameworkDSYMs(archivePath, numberOfFrameworkDSYMs) + if err != nil { + return "", err + } + + return archivePath, nil +} + +func createAppDSYMs(archivePath string, numberOfDSYMs int) error { + return createDSYMs(archivePath, "app", numberOfDSYMs) +} + +func createFrameworkDSYMs(archivePath string, numberOfDSYMs int) error { + return createDSYMs(archivePath, "framework", numberOfDSYMs) +} + +func createDSYMs(archivePath, dSYMType string, numberOfDSYMs int) error { + for i := 0; i < numberOfDSYMs; i++ { + err := os.WriteFile(createDSYMFilePath(archivePath, dSYMType, i), nil, 0777) + if err != nil { + return err + } + } + + return nil +} + +func createDSYMFilePath(archivePath, dSYMType string, index int) string { + return filepath.Join(archivePath, DSYMSDirName, fmt.Sprintf("ios-%d.%s.dSYM", index, dSYMType)) +} + +func createArchive(archivePath string) (string, error) { + tempDirPath, err := pathutil.NormalizedOSTempDirPath(tempDirName) + if err != nil { + return "", err + } + + archivePath = filepath.Join(tempDirPath, archivePath) + err = os.MkdirAll(filepath.Join(archivePath, DSYMSDirName), 0755) + if err != nil { + return "", err + } + + return archivePath, nil +} diff --git a/xcarchive/xcarchive.go b/xcarchive/xcarchive.go new file mode 100644 index 00000000..c25be38b --- /dev/null +++ b/xcarchive/xcarchive.go @@ -0,0 +1,43 @@ +package xcarchive + +import ( + "path/filepath" + + "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-xcode/plistutil" +) + +type ArchiveReader struct{} + +// IsMacOS try to find the Contents dir under the .app/. +// If its finds it the archive is macOS. If it does not the archive is iOS. +func (r ArchiveReader) IsMacOS(archPath string) (bool, error) { + log.Debugf("Checking archive is MacOS or iOS") + infoPlistPath := filepath.Join(archPath, "Info.plist") + + plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + if err != nil { + return false, err + } + + appProperties, found := plist.GetMapStringInterface("ApplicationProperties") + if !found { + return false, err + } + + applicationPath, found := appProperties.GetString("ApplicationPath") + if !found { + return false, err + } + + applicationPath = filepath.Join(archPath, "Products", applicationPath) + contentsPath := filepath.Join(applicationPath, "Contents") + + exist, err := pathutil.IsDirExists(contentsPath) + if err != nil { + return false, err + } + + return exist, nil +} diff --git a/xcarchive/xcarchive_test.go b/xcarchive/xcarchive_test.go new file mode 100644 index 00000000..7d244a82 --- /dev/null +++ b/xcarchive/xcarchive_test.go @@ -0,0 +1,45 @@ +package xcarchive + +import ( + "path/filepath" + "testing" +) + +const ( + tempDirName = "__artifacts__" + DSYMSDirName = "dSYMs" +) + +func TestIsMacOS(t *testing.T) { + tests := []struct { + name string + archPath string + want bool + wantErr bool + }{ + { + name: "macOS", + archPath: filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive"), + want: true, + wantErr: false, + }, + { + name: "iOS", + archPath: filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive"), + want: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ArchiveReader{}.IsMacOS(tt.archPath) + if (err != nil) != tt.wantErr { + t.Errorf("IsMacOS() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("IsMacOS() = %v, want %v", got, tt.want) + } + }) + } +} From 38c03ef5f76754d15a50753f23ed06079b2173a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krisztia=CC=81n=20Go=CC=88drei?= Date: Fri, 1 Aug 2025 11:07:14 +0200 Subject: [PATCH 2/6] Fix lint issue --- xcarchive/xcarchive.go | 1 + 1 file changed, 1 insertion(+) diff --git a/xcarchive/xcarchive.go b/xcarchive/xcarchive.go index c25be38b..3ce3dae9 100644 --- a/xcarchive/xcarchive.go +++ b/xcarchive/xcarchive.go @@ -8,6 +8,7 @@ import ( "github.com/bitrise-io/go-xcode/plistutil" ) +// ArchiveReader ... type ArchiveReader struct{} // IsMacOS try to find the Contents dir under the .app/. From 73714a790417a07968d1ca70cd13c8006d3eac03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krisztia=CC=81n=20Go=CC=88drei?= Date: Fri, 1 Aug 2025 13:40:07 +0200 Subject: [PATCH 3/6] Migrate xcarchive package to go-utils/v2 --- xcarchive/ios.go | 31 +++++++++++------ xcarchive/ios_test.go | 19 ++++------- xcarchive/macos.go | 23 ++++++++----- xcarchive/utils.go | 75 +++++++++++++++++++++++++++++++++++++---- xcarchive/utils_test.go | 15 +++++---- xcarchive/xcarchive.go | 20 ++++++++--- 6 files changed, 134 insertions(+), 49 deletions(-) diff --git a/xcarchive/ios.go b/xcarchive/ios.go index 56108aa4..b92b1801 100644 --- a/xcarchive/ios.go +++ b/xcarchive/ios.go @@ -5,7 +5,9 @@ import ( "fmt" "path/filepath" - "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-utils/v2/command" + "github.com/bitrise-io/go-utils/v2/env" + "github.com/bitrise-io/go-utils/v2/pathutil" "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" @@ -27,10 +29,14 @@ func (app IosBaseApplication) BundleIdentifier() string { // NewIosBaseApplication ... func NewIosBaseApplication(path string) (IosBaseApplication, error) { + pathChecker := pathutil.NewPathChecker() + envRepo := env.NewRepository() + cmdFactory := command.NewFactory(envRepo) + var infoPlist plistutil.PlistData { infoPlistPath := filepath.Join(path, "Info.plist") - if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { + if exist, err := pathChecker.IsPathExists(infoPlistPath); err != nil { return IosBaseApplication{}, fmt.Errorf("failed to check if Info.plist exists at: %s, error: %s", infoPlistPath, err) } else if !exist { return IosBaseApplication{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) @@ -45,7 +51,7 @@ func NewIosBaseApplication(path string) (IosBaseApplication, error) { var provisioningProfile profileutil.ProvisioningProfileInfoModel { provisioningProfilePath := filepath.Join(path, "embedded.mobileprovision") - if exist, err := pathutil.IsPathExists(provisioningProfilePath); err != nil { + if exist, err := pathChecker.IsPathExists(provisioningProfilePath); err != nil { return IosBaseApplication{}, fmt.Errorf("failed to check if profile exists at: %s, error: %s", provisioningProfilePath, err) } else if !exist { return IosBaseApplication{}, fmt.Errorf("profile not exists at: %s", provisioningProfilePath) @@ -59,7 +65,7 @@ func NewIosBaseApplication(path string) (IosBaseApplication, error) { } executable := executableNameFromInfoPlist(infoPlist) - entitlements, err := getEntitlements(path, executable) + entitlements, err := getEntitlements(cmdFactory, path, executable) if err != nil { return IosBaseApplication{}, err } @@ -102,13 +108,14 @@ type IosClipApplication struct { // NewIosWatchApplication ... func NewIosWatchApplication(path string) (IosWatchApplication, error) { + baseApp, err := NewIosBaseApplication(path) if err != nil { return IosWatchApplication{}, err } extensions := []IosExtension{} - pattern := filepath.Join(pathutil.EscapeGlobPath(path), "PlugIns/*.appex") + pattern := filepath.Join(escapeGlobPath(path), "PlugIns/*.appex") pths, err := filepath.Glob(pattern) if err != nil { return IosWatchApplication{}, fmt.Errorf("failed to search for watch application's extensions using pattern: %s, error: %s", pattern, err) @@ -157,7 +164,7 @@ func NewIosApplication(path string) (IosApplication, error) { var watchApp *IosWatchApplication { - pattern := filepath.Join(pathutil.EscapeGlobPath(path), "Watch/*.app") + pattern := filepath.Join(escapeGlobPath(path), "Watch/*.app") pths, err := filepath.Glob(pattern) if err != nil { return IosApplication{}, err @@ -174,7 +181,7 @@ func NewIosApplication(path string) (IosApplication, error) { var clipApp *IosClipApplication { - pattern := filepath.Join(pathutil.EscapeGlobPath(path), "AppClips/*.app") + pattern := filepath.Join(escapeGlobPath(path), "AppClips/*.app") pths, err := filepath.Glob(pattern) if err != nil { return IosApplication{}, err @@ -191,7 +198,7 @@ func NewIosApplication(path string) (IosApplication, error) { extensions := []IosExtension{} { - pattern := filepath.Join(pathutil.EscapeGlobPath(path), "PlugIns/*.appex") + pattern := filepath.Join(escapeGlobPath(path), "PlugIns/*.appex") pths, err := filepath.Glob(pattern) if err != nil { return IosApplication{}, fmt.Errorf("failed to search for watch application's extensions using pattern: %s, error: %s", pattern, err) @@ -223,10 +230,12 @@ type IosArchive struct { // NewIosArchive ... func NewIosArchive(path string) (IosArchive, error) { + pathChecker := pathutil.NewPathChecker() + var infoPlist plistutil.PlistData { infoPlistPath := filepath.Join(path, "Info.plist") - if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { + if exist, err := pathChecker.IsPathExists(infoPlistPath); err != nil { return IosArchive{}, fmt.Errorf("failed to check if Info.plist exists at: %s, error: %s", infoPlistPath, err) } else if !exist { return IosArchive{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) @@ -249,7 +258,7 @@ func NewIosArchive(path string) (IosArchive, error) { return IosArchive{}, err } } - if exist, err := pathutil.IsPathExists(appPath); err != nil { + if exist, err := pathChecker.IsPathExists(appPath); err != nil { return IosArchive{}, fmt.Errorf("failed to check if app exists, path: %s, error: %s", appPath, err) } else if !exist { return IosArchive{}, fmt.Errorf("application not found on path: %s, error: %s", appPath, err) @@ -277,7 +286,7 @@ func applicationFromPlist(InfoPlist plistutil.PlistData) (string, bool) { } func applicationFromArchive(path string) (string, error) { - pattern := filepath.Join(pathutil.EscapeGlobPath(path), "Products/Applications/*.app") + pattern := filepath.Join(escapeGlobPath(path), "Products/Applications/*.app") pths, err := filepath.Glob(pattern) if err != nil { return "", err diff --git a/xcarchive/ios_test.go b/xcarchive/ios_test.go index 81d0d94c..31e34398 100644 --- a/xcarchive/ios_test.go +++ b/xcarchive/ios_test.go @@ -7,8 +7,8 @@ import ( "reflect" "testing" - "github.com/bitrise-io/go-utils/command" - "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-utils/v2/command" + "github.com/bitrise-io/go-utils/v2/env" "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" @@ -22,11 +22,10 @@ func sampleRepoPath(t *testing.T) string { if tmpDir != "" { dir = tmpDir } else { - var err error - dir, err = pathutil.NormalizedOSTempDirPath(tempDirName) - require.NoError(t, err) + dir = t.TempDir() sampleArtifactsGitURI := "https://github.com/bitrise-io/sample-artifacts.git" - cmd := command.New("git", "clone", sampleArtifactsGitURI, dir) + + cmd := command.NewFactory(env.NewRepository()).Create("git", []string{"clone", sampleArtifactsGitURI, dir}, nil) output, err := cmd.RunAndReturnTrimmedCombinedOutput() if err != nil { t.Log(output) @@ -165,16 +164,12 @@ func TestFindDSYMs(t *testing.T) { } func Test_applicationFromArchive(t *testing.T) { - var err error - tempDir, err := pathutil.NormalizedOSTempDirPath(t.Name()) - if err != nil { - t.Errorf("setup: failed to create temp dir") - } + tempDir := t.TempDir() archivePath := filepath.Join(tempDir, "{}GlobControlChars:a-b[ab]?*", "test.xcarchive") appDir := filepath.Join(archivePath, "Products", "Applications") appPath := filepath.Join(appDir, "test.app") t.Logf("Test app path: %s", appPath) - err = os.MkdirAll(appDir, os.ModePerm) + err := os.MkdirAll(appDir, os.ModePerm) if err != nil { t.Errorf("setup: failed to create directory: %s, error: %s", appDir, err) } diff --git a/xcarchive/macos.go b/xcarchive/macos.go index 929a6ce4..2cdf1068 100644 --- a/xcarchive/macos.go +++ b/xcarchive/macos.go @@ -4,10 +4,11 @@ import ( "fmt" "path/filepath" + "github.com/bitrise-io/go-utils/v2/command" + "github.com/bitrise-io/go-utils/v2/env" + "github.com/bitrise-io/go-utils/v2/pathutil" "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/profileutil" - - "github.com/bitrise-io/go-utils/pathutil" ) type macosBaseApplication struct { @@ -24,10 +25,14 @@ func (app macosBaseApplication) BundleIdentifier() string { } func newMacosBaseApplication(path string) (macosBaseApplication, error) { + pathChecker := pathutil.NewPathChecker() + envRepo := env.NewRepository() + cmdFactory := command.NewFactory(envRepo) + var infoPlist plistutil.PlistData { infoPlistPath := filepath.Join(path, "Contents/Info.plist") - if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { + if exist, err := pathChecker.IsPathExists(infoPlistPath); err != nil { return macosBaseApplication{}, fmt.Errorf("failed to check if Info.plist exists at: %s, error: %s", infoPlistPath, err) } else if !exist { return macosBaseApplication{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) @@ -42,7 +47,7 @@ func newMacosBaseApplication(path string) (macosBaseApplication, error) { var provisioningProfile *profileutil.ProvisioningProfileInfoModel { provisioningProfilePath := filepath.Join(path, "Contents/embedded.provisionprofile") - if exist, err := pathutil.IsPathExists(provisioningProfilePath); err != nil { + if exist, err := pathChecker.IsPathExists(provisioningProfilePath); err != nil { return macosBaseApplication{}, fmt.Errorf("failed to check if profile exists at: %s, error: %s", provisioningProfilePath, err) } else if exist { profile, err := profileutil.NewProvisioningProfileInfoFromFile(provisioningProfilePath) @@ -54,7 +59,7 @@ func newMacosBaseApplication(path string) (macosBaseApplication, error) { } executable := filepath.Join("/Contents/MacOS/", executableNameFromInfoPlist(infoPlist)) - entitlements, err := getEntitlements(path, executable) + entitlements, err := getEntitlements(cmdFactory, path, executable) if err != nil { return macosBaseApplication{}, err } @@ -99,7 +104,7 @@ func NewMacosApplication(path string) (MacosApplication, error) { extensions := []MacosExtension{} { - pattern := filepath.Join(pathutil.EscapeGlobPath(path), "Contents/PlugIns/*.appex") + pattern := filepath.Join(escapeGlobPath(path), "Contents/PlugIns/*.appex") pths, err := filepath.Glob(pattern) if err != nil { return MacosApplication{}, fmt.Errorf("failed to search for watch application's extensions using pattern: %s, error: %s", pattern, err) @@ -129,10 +134,12 @@ type MacosArchive struct { // NewMacosArchive ... func NewMacosArchive(path string) (MacosArchive, error) { + pathChecker := pathutil.NewPathChecker() + var infoPlist plistutil.PlistData { infoPlistPath := filepath.Join(path, "Info.plist") - if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { + if exist, err := pathChecker.IsPathExists(infoPlistPath); err != nil { return MacosArchive{}, fmt.Errorf("failed to check if Info.plist exists at: %s, error: %s", infoPlistPath, err) } else if !exist { return MacosArchive{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) @@ -146,7 +153,7 @@ func NewMacosArchive(path string) (MacosArchive, error) { var application MacosApplication { - pattern := filepath.Join(pathutil.EscapeGlobPath(path), "Products/Applications/*.app") + pattern := filepath.Join(escapeGlobPath(path), "Products/Applications/*.app") pths, err := filepath.Glob(pattern) if err != nil { return MacosArchive{}, err diff --git a/xcarchive/utils.go b/xcarchive/utils.go index 987257c7..b2da5b38 100644 --- a/xcarchive/utils.go +++ b/xcarchive/utils.go @@ -2,11 +2,11 @@ package xcarchive import ( "fmt" + "io/ioutil" "path/filepath" "strings" - "github.com/bitrise-io/go-utils/command" - "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-utils/v2/command" "github.com/bitrise-io/go-xcode/plistutil" ) @@ -17,8 +17,8 @@ func executableNameFromInfoPlist(infoPlist plistutil.PlistData) string { return "" } -func getEntitlements(basePath, executableRelativePath string) (plistutil.PlistData, error) { - entitlements, err := entitlementsFromExecutable(basePath, executableRelativePath) +func getEntitlements(cmdFactory command.Factory, basePath, executableRelativePath string) (plistutil.PlistData, error) { + entitlements, err := entitlementsFromExecutable(cmdFactory, basePath, executableRelativePath) if err != nil { return plistutil.PlistData{}, err } @@ -30,10 +30,10 @@ func getEntitlements(basePath, executableRelativePath string) (plistutil.PlistDa return plistutil.PlistData{}, nil } -func entitlementsFromExecutable(basePath, executableRelativePath string) (*plistutil.PlistData, error) { +func entitlementsFromExecutable(cmdFactory command.Factory, basePath, executableRelativePath string) (*plistutil.PlistData, error) { fmt.Printf("Fetching entitlements from executable") - cmd := command.New("codesign", "--display", "--entitlements", ":-", filepath.Join(basePath, executableRelativePath)) + cmd := cmdFactory.Create("codesign", []string{"--display", "--entitlements", ":-", filepath.Join(basePath, executableRelativePath)}, nil) entitlementsString, err := cmd.RunAndReturnTrimmedOutput() if err != nil { return nil, err @@ -49,7 +49,7 @@ func entitlementsFromExecutable(basePath, executableRelativePath string) (*plist func findDSYMs(archivePath string) ([]string, []string, error) { dsymsDirPth := filepath.Join(archivePath, "dSYMs") - dsyms, err := pathutil.ListEntries(dsymsDirPth, pathutil.ExtensionFilter(".dsym", true)) + dsyms, err := listEntries(dsymsDirPth, extensionFilter(".dsym", true)) if err != nil { return []string{}, []string{}, err } @@ -66,3 +66,64 @@ func findDSYMs(archivePath string) ([]string, []string, error) { return appDSYMs, frameworkDSYMs, nil } + +func escapeGlobPath(path string) string { + var escaped string + for _, ch := range path { + if ch == '[' || ch == ']' || ch == '-' || ch == '*' || ch == '?' || ch == '\\' { + escaped += "\\" + } + escaped += string(ch) + } + return escaped +} + +type filterFunc func(string) (bool, error) + +func listEntries(dir string, filters ...filterFunc) ([]string, error) { + absDir, err := filepath.Abs(dir) + if err != nil { + return []string{}, err + } + + entries, err := ioutil.ReadDir(absDir) + if err != nil { + return []string{}, err + } + + var paths []string + for _, entry := range entries { + pth := filepath.Join(absDir, entry.Name()) + paths = append(paths, pth) + } + + return filterPaths(paths, filters...) +} + +func filterPaths(fileList []string, filters ...filterFunc) ([]string, error) { + var filtered []string + + for _, pth := range fileList { + allowed := true + for _, filter := range filters { + if allows, err := filter(pth); err != nil { + return []string{}, err + } else if !allows { + allowed = false + break + } + } + if allowed { + filtered = append(filtered, pth) + } + } + + return filtered, nil +} + +func extensionFilter(ext string, allowed bool) filterFunc { + return func(pth string) (bool, error) { + e := filepath.Ext(pth) + return allowed == strings.EqualFold(ext, e), nil + } +} diff --git a/xcarchive/utils_test.go b/xcarchive/utils_test.go index aeca2047..756246fa 100644 --- a/xcarchive/utils_test.go +++ b/xcarchive/utils_test.go @@ -6,19 +6,21 @@ import ( "path/filepath" "testing" - "github.com/bitrise-io/go-utils/pathutil" - + "github.com/bitrise-io/go-utils/v2/command" + "github.com/bitrise-io/go-utils/v2/env" + "github.com/bitrise-io/go-utils/v2/pathutil" "github.com/bitrise-io/go-xcode/plistutil" "github.com/stretchr/testify/assert" ) func TestGiveniOS_WhenAskingForEntitlements_ThenReadsItFromTheExecutable(t *testing.T) { // Given + cmdFactory := command.NewFactory(env.NewRepository()) appPath := filepath.Join(sampleRepoPath(t), "archives/Fruta.xcarchive/Products/Applications/Fruta.app") executable := executableRelativePath(appPath, "Info.plist", "") // When - entitlements, err := getEntitlements(appPath, executable) + entitlements, err := getEntitlements(cmdFactory, appPath, executable) // Then assert.NoError(t, err) @@ -27,11 +29,12 @@ func TestGiveniOS_WhenAskingForEntitlements_ThenReadsItFromTheExecutable(t *test func TestGivenMacos_WhenAskingForEntitlements_ThenReadsItFromTheExecutable(t *testing.T) { // Given + cmdFactory := command.NewFactory(env.NewRepository()) appPath := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive/Products/Applications/Test.app") executable := executableRelativePath(appPath, "Contents/Info.plist", "Contents/MacOS/") // When - entitlements, err := getEntitlements(appPath, executable) + entitlements, err := getEntitlements(cmdFactory, appPath, executable) // Then assert.NoError(t, err) @@ -40,7 +43,7 @@ func TestGivenMacos_WhenAskingForEntitlements_ThenReadsItFromTheExecutable(t *te func executableRelativePath(basePath, infoPlistRelativePath, executableFolderRelativePath string) string { infoPlistPath := filepath.Join(basePath, infoPlistRelativePath) - exist, err := pathutil.IsPathExists(infoPlistPath) + exist, err := pathutil.NewPathChecker().IsPathExists(infoPlistPath) if err != nil { return "" } @@ -177,7 +180,7 @@ func createDSYMFilePath(archivePath, dSYMType string, index int) string { } func createArchive(archivePath string) (string, error) { - tempDirPath, err := pathutil.NormalizedOSTempDirPath(tempDirName) + tempDirPath, err := pathutil.NewPathProvider().CreateTempDir(tempDirName) if err != nil { return "", err } diff --git a/xcarchive/xcarchive.go b/xcarchive/xcarchive.go index 3ce3dae9..f55deae9 100644 --- a/xcarchive/xcarchive.go +++ b/xcarchive/xcarchive.go @@ -3,18 +3,28 @@ package xcarchive import ( "path/filepath" - "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-utils/v2/log" + "github.com/bitrise-io/go-utils/v2/pathutil" "github.com/bitrise-io/go-xcode/plistutil" ) // ArchiveReader ... -type ArchiveReader struct{} +type ArchiveReader struct { + pathChecker pathutil.PathChecker + logger log.Logger +} + +func NewArchiveReader(pathChecker pathutil.PathChecker, logger log.Logger) ArchiveReader { + return ArchiveReader{ + pathChecker: pathChecker, + logger: logger, + } +} // IsMacOS try to find the Contents dir under the .app/. // If its finds it the archive is macOS. If it does not the archive is iOS. func (r ArchiveReader) IsMacOS(archPath string) (bool, error) { - log.Debugf("Checking archive is MacOS or iOS") + r.logger.Debugf("Checking archive is MacOS or iOS") infoPlistPath := filepath.Join(archPath, "Info.plist") plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) @@ -35,7 +45,7 @@ func (r ArchiveReader) IsMacOS(archPath string) (bool, error) { applicationPath = filepath.Join(archPath, "Products", applicationPath) contentsPath := filepath.Join(applicationPath, "Contents") - exist, err := pathutil.IsDirExists(contentsPath) + exist, err := r.pathChecker.IsDirExists(contentsPath) if err != nil { return false, err } From c9ae14b2fce5585c42d609605957d8fa105e61a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krisztia=CC=81n=20Go=CC=88drei?= Date: Fri, 1 Aug 2025 13:43:55 +0200 Subject: [PATCH 4/6] Fix lint issues --- xcarchive/utils.go | 4 ++-- xcarchive/xcarchive.go | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/xcarchive/utils.go b/xcarchive/utils.go index b2da5b38..d7958dde 100644 --- a/xcarchive/utils.go +++ b/xcarchive/utils.go @@ -2,7 +2,7 @@ package xcarchive import ( "fmt" - "io/ioutil" + "os" "path/filepath" "strings" @@ -86,7 +86,7 @@ func listEntries(dir string, filters ...filterFunc) ([]string, error) { return []string{}, err } - entries, err := ioutil.ReadDir(absDir) + entries, err := os.ReadDir(absDir) if err != nil { return []string{}, err } diff --git a/xcarchive/xcarchive.go b/xcarchive/xcarchive.go index f55deae9..66015747 100644 --- a/xcarchive/xcarchive.go +++ b/xcarchive/xcarchive.go @@ -14,6 +14,7 @@ type ArchiveReader struct { logger log.Logger } +// NewArchiveReader ... func NewArchiveReader(pathChecker pathutil.PathChecker, logger log.Logger) ArchiveReader { return ArchiveReader{ pathChecker: pathChecker, From 2dc0b27d6cfa510921130361e745d4fba5adf5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krisztia=CC=81n=20Go=CC=88drei?= Date: Fri, 1 Aug 2025 14:12:35 +0200 Subject: [PATCH 5/6] Fix xcarchive tests --- xcarchive/ios_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/xcarchive/ios_test.go b/xcarchive/ios_test.go index 31e34398..1172fad2 100644 --- a/xcarchive/ios_test.go +++ b/xcarchive/ios_test.go @@ -9,6 +9,7 @@ import ( "github.com/bitrise-io/go-utils/v2/command" "github.com/bitrise-io/go-utils/v2/env" + "github.com/bitrise-io/go-utils/v2/pathutil" "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" @@ -22,7 +23,9 @@ func sampleRepoPath(t *testing.T) string { if tmpDir != "" { dir = tmpDir } else { - dir = t.TempDir() + var err error + dir, err = pathutil.NewPathProvider().CreateTempDir(tempDirName) + require.NoError(t, err) sampleArtifactsGitURI := "https://github.com/bitrise-io/sample-artifacts.git" cmd := command.NewFactory(env.NewRepository()).Create("git", []string{"clone", sampleArtifactsGitURI, dir}, nil) From cc9755f70ed685be7fbf7d376f38a875cb1bcff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krisztia=CC=81n=20Go=CC=88drei?= Date: Fri, 1 Aug 2025 15:29:40 +0200 Subject: [PATCH 6/6] Fix xcarchive tests --- xcarchive/xcarchive_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/xcarchive/xcarchive_test.go b/xcarchive/xcarchive_test.go index 7d244a82..2654e2a0 100644 --- a/xcarchive/xcarchive_test.go +++ b/xcarchive/xcarchive_test.go @@ -3,6 +3,9 @@ package xcarchive import ( "path/filepath" "testing" + + "github.com/bitrise-io/go-utils/v2/log" + "github.com/bitrise-io/go-utils/v2/pathutil" ) const ( @@ -32,7 +35,10 @@ func TestIsMacOS(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ArchiveReader{}.IsMacOS(tt.archPath) + pathChecker := pathutil.NewPathChecker() + logger := log.NewLogger() + archiveReader := NewArchiveReader(pathChecker, logger) + got, err := archiveReader.IsMacOS(tt.archPath) if (err != nil) != tt.wantErr { t.Errorf("IsMacOS() error = %v, wantErr %v", err, tt.wantErr) return