diff --git a/cmd/xcode.go b/cmd/xcode.go index ee7ed686..49616b9d 100644 --- a/cmd/xcode.go +++ b/cmd/xcode.go @@ -8,6 +8,7 @@ import ( "github.com/bitrise-io/codesigndoc/codesign" "github.com/bitrise-io/codesigndoc/codesigndoc" + "github.com/bitrise-io/codesigndoc/utility" "github.com/bitrise-io/codesigndoc/xcode" "github.com/bitrise-io/go-utils/colorstring" "github.com/bitrise-io/go-utils/fileutil" @@ -32,6 +33,7 @@ var ( paramXcodeProjectFilePath string paramXcodeScheme string paramXcodebuildSDK string + paramXcodeDestination string ) func init() { @@ -40,6 +42,7 @@ func init() { xcodeCmd.Flags().StringVar(¶mXcodeProjectFilePath, "file", "", "Xcode Project/Workspace file path") xcodeCmd.Flags().StringVar(¶mXcodeScheme, "scheme", "", "Xcode Scheme") xcodeCmd.Flags().StringVar(¶mXcodebuildSDK, "xcodebuild-sdk", "", "xcodebuild -sdk param. If a value is specified for this flag it'll be passed to xcodebuild as the value of the -sdk flag. For more info about the values please see xcodebuild's -sdk flag docs. Example value: iphoneos") + xcodeCmd.Flags().StringVar(¶mXcodeDestination, "xcodebuild-destination", "", "The xcodebuild -destination option takes as its argument a destination specifier describing the device (or devices) to use as a destination i.e `generic/platform=iOS`. If a value is specified for this flag it'll be passed to xcodebuild.") } func absOutputDir() (string, error) { @@ -108,6 +111,24 @@ func scanXcodeProject(_ *cobra.Command, _ []string) error { xcodeCmd.SDK = paramXcodebuildSDK } + if paramXcodeDestination != "" { + xcodeCmd.Destination = paramXcodeDestination + } else { + project, scheme, configuration, err := utility.OpenArchivableProject(xcodeCmd.ProjectFilePath, xcodeCmd.Scheme, "") + if err != nil { + return err + } + + platform, err := utility.BuildableTargetPlatform(project, scheme, configuration, utility.XcodeBuild{}) + if err == nil { + destination := "generic/platform=" + string(platform) + + xcodeCmd.Destination = destination + + fmt.Print("Setting xcodebuild -destination flag to: ", destination) + } + } + writeBuildLogs := func(xcodebuildOutput string) error { if writeFiles == codesign.WriteFilesAlways || writeFiles == codesign.WriteFilesFallback && err != nil { // save the xcodebuild output into a debug log file xcodebuildOutputFilePath := filepath.Join(absExportOutputDirPath, "xcodebuild-output.log") diff --git a/cmd/xcodeUITests.go b/cmd/xcodeUITests.go index 5576b28b..fc042759 100644 --- a/cmd/xcodeUITests.go +++ b/cmd/xcodeUITests.go @@ -8,6 +8,7 @@ import ( "github.com/bitrise-io/codesigndoc/codesign" "github.com/bitrise-io/codesigndoc/codesigndocuitests" + codesigndocutility "github.com/bitrise-io/codesigndoc/utility" "github.com/bitrise-io/codesigndoc/xcodeuitest" "github.com/bitrise-io/go-utils/colorstring" "github.com/bitrise-io/go-utils/fileutil" @@ -34,6 +35,7 @@ func init() { xcodeUITestsCmd.Flags().StringVar(¶mXcodeProjectFilePath, "file", "", "Xcode Project/Workspace file path") xcodeUITestsCmd.Flags().StringVar(¶mXcodeScheme, "scheme", "", "Xcode Scheme") xcodeUITestsCmd.Flags().StringVar(¶mXcodebuildSDK, "xcodebuild-sdk", "", "xcodebuild -sdk param. If a value is specified for this flag it'll be passed to xcodebuild as the value of the -sdk flag. For more info about the values please see xcodebuild's -sdk flag docs. Example value: iphoneos") + xcodeUITestsCmd.Flags().StringVar(¶mXcodeDestination, "xcodebuild-destination", "", "The xcodebuild -destination option takes as its argument a destination specifier describing the device (or devices) to use as a destination i.e `generic/platform=iOS`. If a value is specified for this flag it'll be passed to xcodebuild.") } func scanXcodeUITestsProject(cmd *cobra.Command, args []string) error { @@ -114,6 +116,24 @@ func scanXcodeUITestsProject(cmd *cobra.Command, args []string) error { xcodeUITestsCmd.SDK = paramXcodebuildSDK } + if paramXcodeDestination != "" { + xcodeUITestsCmd.Destination = paramXcodeDestination + } else { + project, scheme, configuration, err := codesigndocutility.OpenArchivableProject(xcodeUITestsCmd.ProjectFilePath, xcodeUITestsCmd.Scheme, "") + if err != nil { + return err + } + + platform, err := codesigndocutility.BuildableTargetPlatform(project, scheme, configuration, codesigndocutility.XcodeBuild{}) + if err == nil { + destination := "generic/platform=" + string(platform) + + xcodeUITestsCmd.Destination = destination + + fmt.Print("Setting xcodebuild -destination flag to: ", destination) + } + } + fmt.Println() fmt.Println() log.Printf("🔦 Running an Xcode build-for-testing, to get all the required code signing settings...") diff --git a/utility/utility.go b/utility/utility.go index 3055403c..a56af5fc 100644 --- a/utility/utility.go +++ b/utility/utility.go @@ -1,10 +1,17 @@ package utility import ( + "fmt" + "path/filepath" "regexp" + "strings" "github.com/bitrise-io/go-utils/log" "github.com/bitrise-io/go-xcode/profileutil" + "github.com/bitrise-io/go-xcode/xcodeproject/schemeint" + "github.com/bitrise-io/go-xcode/xcodeproject/serialized" + "github.com/bitrise-io/go-xcode/xcodeproject/xcodeproj" + "github.com/bitrise-io/go-xcode/xcodeproject/xcscheme" ) // ProfileExportFileNameNoPath creates a file name for the given profile with pattern: uuid.escaped_profile_name.[mobileprovision|provisionprofile] @@ -22,3 +29,129 @@ func ProfileExportFileNameNoPath(info profileutil.ProvisioningProfileInfoModel) return info.UUID + "." + safeTitle + extension } + +// Platform ... +type Platform string + +const ( + iOS Platform = "iOS" + osX Platform = "OS X" + tvOS Platform = "tvOS" + watchOS Platform = "watchOS" +) + +// TargetBuildSettingsProvider ... +type TargetBuildSettingsProvider interface { + TargetBuildSettings(xcodeProj *xcodeproj.XcodeProj, target, configuration string, customOptions ...string) (serialized.Object, error) +} + +// XcodeBuild ... +type XcodeBuild struct { +} + +// TargetBuildSettings ... +func (x XcodeBuild) TargetBuildSettings(xcodeProj *xcodeproj.XcodeProj, target, configuration string, customOptions ...string) (serialized.Object, error) { + return xcodeProj.TargetBuildSettings(target, configuration, customOptions...) +} + +// BuildableTargetPlatform ... +func BuildableTargetPlatform( + xcodeProj *xcodeproj.XcodeProj, + scheme *xcscheme.Scheme, + configurationName string, + provider TargetBuildSettingsProvider, +) (Platform, error) { + archiveEntry, ok := scheme.AppBuildActionEntry() + if !ok { + return "", fmt.Errorf("archivable entry not found in project: %s, scheme: %s", xcodeProj.Path, scheme.Name) + } + + mainTarget, ok := xcodeProj.Proj.Target(archiveEntry.BuildableReference.BlueprintIdentifier) + if !ok { + return "", fmt.Errorf("target not found: %s", archiveEntry.BuildableReference.BlueprintIdentifier) + } + + settings, err := provider.TargetBuildSettings(xcodeProj, mainTarget.Name, configurationName) + if err != nil { + return "", fmt.Errorf("failed to get target (%s) build settings: %s", mainTarget.Name, err) + } + + return getPlatform(settings) +} + +func getPlatform(buildSettings serialized.Object) (Platform, error) { + /* + Xcode help: + Base SDK (SDKROOT) + The name or path of the base SDK being used during the build. + The product will be built against the headers and libraries located inside the indicated SDK. + This path will be prepended to all search paths, and will be passed through the environment to the compiler and linker. + Additional SDKs can be specified in the Additional SDKs (ADDITIONAL_SDKS) setting. + + Examples: + - /Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk + - /Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator13.4.sdk + - /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.4.sdk + - /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk + - /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk + - /Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk + - /Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk + - iphoneos + - macosx + - appletvos + - watchos + */ + sdk, err := buildSettings.String("SDKROOT") + if err != nil { + return "", fmt.Errorf("failed to get SDKROOT: %s", err) + } + + sdk = strings.ToLower(sdk) + if filepath.Ext(sdk) == ".sdk" { + sdk = filepath.Base(sdk) + } + + switch { + case strings.HasPrefix(sdk, "iphoneos"): + return iOS, nil + case strings.HasPrefix(sdk, "macosx"): + return osX, nil + case strings.HasPrefix(sdk, "appletvos"): + return tvOS, nil + case strings.HasPrefix(sdk, "watchos"): + return watchOS, nil + default: + return "", fmt.Errorf("unkown SDKROOT: %s", sdk) + } +} + +// OpenArchivableProject ... +func OpenArchivableProject(pth, schemeName, configurationName string) (*xcodeproj.XcodeProj, *xcscheme.Scheme, string, error) { + scheme, schemeContainerDir, err := schemeint.Scheme(pth, schemeName) + if err != nil { + return nil, nil, "", fmt.Errorf("could not get scheme (%s) from path (%s): %s", schemeName, pth, err) + } + if configurationName == "" { + configurationName = scheme.ArchiveAction.BuildConfiguration + } + + if configurationName == "" { + return nil, nil, "", fmt.Errorf("no configuration provided nor default defined for the scheme's (%s) archive action", schemeName) + } + + archiveEntry, ok := scheme.AppBuildActionEntry() + if !ok { + return nil, nil, "", fmt.Errorf("archivable entry not found") + } + + projectPth, err := archiveEntry.BuildableReference.ReferencedContainerAbsPath(filepath.Dir(schemeContainerDir)) + if err != nil { + return nil, nil, "", err + } + + xcodeProj, err := xcodeproj.Open(projectPth) + if err != nil { + return nil, nil, "", err + } + return &xcodeProj, scheme, configurationName, nil +} diff --git a/utility/utility_test.go b/utility/utility_test.go new file mode 100644 index 00000000..faab2fe1 --- /dev/null +++ b/utility/utility_test.go @@ -0,0 +1,36 @@ +package utility + +import ( + "testing" + + "github.com/bitrise-io/go-xcode/xcodeproject/serialized" + "github.com/stretchr/testify/require" +) + +func TestPlatformsMatching_iOS(t *testing.T) { + buildSettings := serialized.Object{} + buildSettings["SDKROOT"] = "iphoneos" + + platform, err := getPlatform(buildSettings) + + require.Equal(t, "iOS", string(platform)) + require.Nil(t, err) +} + +func TestPlatformsMatching_macOS(t *testing.T) { + buildSettings := serialized.Object{} + buildSettings["SDKROOT"] = "macosx" + + platform, err := getPlatform(buildSettings) + + require.Equal(t, "OS X", string(platform)) + require.Nil(t, err) +} + +func TestPlatformsMatching_fails(t *testing.T) { + buildSettings := serialized.Object{} + platform, err := getPlatform(buildSettings) + + require.Empty(t, platform) + require.NotNil(t, err) +} diff --git a/xcode/xcodecmd.go b/xcode/xcodecmd.go index dfccd299..cbbf1a27 100644 --- a/xcode/xcodecmd.go +++ b/xcode/xcodecmd.go @@ -35,6 +35,17 @@ type CommandModel struct { // For more info about the possible values please see xcodebuild's docs about the -sdk flag. // Only passed to xcodebuild if not empty! SDK string + + // DESTINATION: configure which device or Simulator will be used by the tool + // The supported platforms are: + // OS X, your Mac + // iOS, a connected iOS device + // iOS Simulator + // watchOS + // watchOS Simulator + // tvOS + // tvOS Simulator + Destination string } // GenerateArchive : generates the archive for subsequent "Scan" @@ -83,6 +94,10 @@ func (xccmd CommandModel) transformToXcodebuildParams(xcodebuildActionArgs ...st baseArgs = append(baseArgs, "-sdk", xccmd.SDK) } + if xccmd.Destination != "" { + baseArgs = append(baseArgs, "-destination", xccmd.Destination) + } + if xccmd.CodeSignIdentity != "" { baseArgs = append(baseArgs, `CODE_SIGN_IDENTITY=`+xccmd.CodeSignIdentity) } diff --git a/xcodeuitest/xcodeuitestcmd.go b/xcodeuitest/xcodeuitestcmd.go index c352219c..a46c53b3 100644 --- a/xcodeuitest/xcodeuitestcmd.go +++ b/xcodeuitest/xcodeuitestcmd.go @@ -34,6 +34,17 @@ type CommandModel struct { // For more info about the possible values please see xcodebuild's docs about the -sdk flag. // Only passed to xcodebuild if not empty! SDK string + + // DESTINATION: configure which device or Simulator will be used by the tool + // The supported platforms are: + // OS X, your Mac + // iOS, a connected iOS device + // iOS Simulator + // watchOS + // watchOS Simulator + // tvOS + // tvOS Simulator + Destination string } // RunBuildForTesting runs the build-for-tesing xcode command @@ -82,12 +93,17 @@ func (xcuitestcmd CommandModel) transformToXcodebuildParams(xcodebuildActionArgs baseArgs = append(baseArgs, "-sdk", xcuitestcmd.SDK) } + if xcuitestcmd.Destination != "" { + baseArgs = append(baseArgs, "-destination", xcuitestcmd.Destination) + } + return append(baseArgs, xcodebuildActionArgs...), nil } // RunXcodebuildCommand TODO comment func (xcuitestcmd CommandModel) RunXcodebuildCommand(xcodebuildActionArgs ...string) (string, error) { xcodeCmdParamsToRun, err := xcuitestcmd.transformToXcodebuildParams(xcodebuildActionArgs...) + if err != nil { return "", err } @@ -126,7 +142,7 @@ func (xcuitestcmd CommandModel) ScanSchemes() (schemes []xcscheme.Scheme, scheme } else { proj, err := xcodeproj.Open(xcuitestcmd.ProjectFilePath) if err != nil { - return nil, nil, fmt.Errorf("Failed to open project (%s), error: %s", xcuitestcmd.ProjectFilePath, err) + return nil, nil, fmt.Errorf("failed to open project (%s), error: %s", xcuitestcmd.ProjectFilePath, err) } schemes, err = proj.Schemes()