// DownloadFile ... func DownloadFile(downloadURL, targetPath string) error { outFile, err := os.Create(targetPath) defer func() { if err := outFile.Close(); err != nil { log.Warn("Failed to close (%s)", targetPath) } }() if err != nil { return fmt.Errorf("failed to create (%s), error: %s", targetPath, err) } resp, err := http.Get(downloadURL) if err != nil { return fmt.Errorf("failed to download from (%s), error: %s", downloadURL, err) } defer func() { if err := resp.Body.Close(); err != nil { log.Warn("failed to close (%s) body", downloadURL) } }() if resp.StatusCode != http.StatusOK { return fmt.Errorf("non success status code: %d", resp.StatusCode) } _, err = io.Copy(outFile, resp.Body) if err != nil { return fmt.Errorf("failed to download from (%s), error: %s", downloadURL, err) } return nil }
func downloadFile(downloadURL, targetPath string) error { outFile, err := os.Create(targetPath) if err != nil { return fmt.Errorf("failed to create (%s), error: %s", targetPath, err) } defer func() { if err := outFile.Close(); err != nil { log.Warn("Failed to close (%s)", targetPath) } }() resp, err := http.Get(downloadURL) if err != nil { return fmt.Errorf("failed to download from (%s), error: %s", downloadURL, err) } defer func() { if err := resp.Body.Close(); err != nil { log.Warn("failed to close (%s) body", downloadURL) } }() _, err = io.Copy(outFile, resp.Body) if err != nil { return fmt.Errorf("failed to download from (%s), error: %s", downloadURL, err) } return nil }
func saveRawOutputToLogFile(rawXcodebuildOutput string, isRunSuccess bool) error { tmpDir, err := pathutil.NormalizedOSTempDirPath("xcodebuild-output") if err != nil { return fmt.Errorf("Failed to create temp dir, error: %s", err) } logFileName := "raw-xcodebuild-output.log" logPth := filepath.Join(tmpDir, logFileName) if err := fileutil.WriteStringToFile(logPth, rawXcodebuildOutput); err != nil { return fmt.Errorf("Failed to write xcodebuild output to file, error: %s", err) } if !isRunSuccess { deployDir := os.Getenv("BITRISE_DEPLOY_DIR") if deployDir == "" { return errors.New("No BITRISE_DEPLOY_DIR found") } deployPth := filepath.Join(deployDir, logFileName) if err := cmdex.CopyFile(logPth, deployPth); err != nil { return fmt.Errorf("Failed to copy xcodebuild output log file from (%s) to (%s), error: %s", logPth, deployPth, err) } logPth = deployPth } if err := cmd.ExportEnvironmentWithEnvman("BITRISE_XCODE_RAW_TEST_RESULT_TEXT_PATH", logPth); err != nil { log.Warn("Failed to export: BITRISE_XCODE_RAW_TEST_RESULT_TEXT_PATH, error: %s", err) } return nil }
func writeBytesToFileWithPermission(pth string, fileCont []byte, perm os.FileMode) error { if pth == "" { return errors.New("No path provided") } var file *os.File var err error if perm == 0 { file, err = os.Create(pth) } else { // same as os.Create, but with a specified permission // the flags are copy-pasted from the official // os.Create func: https://golang.org/src/os/file.go?s=7327:7366#L244 file, err = os.OpenFile(pth, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm) } if err != nil { return err } defer func() { if err := file.Close(); err != nil { log.Warn(" [!] Failed to close file:", err) } }() if _, err := file.Write(fileCont); err != nil { return err } return nil }
func isStringFoundInOutput(searchStr, outputToSearchIn string) bool { r, err := regexp.Compile("(?i)" + searchStr) if err != nil { log.Warn("Failed to compile regexp: %s", err) return false } return r.MatchString(outputToSearchIn) }
// Run ... func (c CommandModel) Run() (string, error) { prettyCmd := c.Command() xcodebuildCmd := c.xcodebuildCommand.Command() // Configure cmd in- and outputs pipeReader, pipeWriter := io.Pipe() var outBuffer bytes.Buffer outWriter := io.MultiWriter(&outBuffer, pipeWriter) xcodebuildCmd.SetStdin(nil) xcodebuildCmd.SetStdout(outWriter) xcodebuildCmd.SetStderr(outWriter) prettyCmd.SetStdin(pipeReader) prettyCmd.SetStdout(os.Stdout) prettyCmd.SetStderr(os.Stdout) // Run if err := xcodebuildCmd.GetCmd().Start(); err != nil { out := outBuffer.String() return out, err } if err := prettyCmd.GetCmd().Start(); err != nil { out := outBuffer.String() return out, err } // Always close xcpretty outputs defer func() { if err := pipeWriter.Close(); err != nil { log.Warn("Failed to close xcodebuild-xcpretty pipe, error: %s", err) } if err := prettyCmd.GetCmd().Wait(); err != nil { log.Warn("xcpretty command failed, error: %s", err) } }() if err := xcodebuildCmd.GetCmd().Wait(); err != nil { out := outBuffer.String() return out, err } return outBuffer.String(), nil }
func (configs ConfigsModel) print() { log.Info("ipa export configs:") useCustomExportOptions := (configs.CustomExportOptionsPlistContent != "") if useCustomExportOptions { fmt.Println() log.Warn("Ignoring the following options because CustomExportOptionsPlistContent provided:") } log.Detail("- ExportMethod: %s", configs.ExportMethod) log.Detail("- UploadBitcode: %s", configs.UploadBitcode) log.Detail("- CompileBitcode: %s", configs.CompileBitcode) log.Detail("- TeamID: %s", configs.TeamID) if useCustomExportOptions { log.Warn("----------") } log.Detail("- UseDeprecatedExport: %s", configs.UseDeprecatedExport) log.Detail("- ForceTeamID: %s", configs.ForceTeamID) log.Detail("- ForceProvisioningProfileSpecifier: %s", configs.ForceProvisioningProfileSpecifier) log.Detail("- ForceProvisioningProfile: %s", configs.ForceProvisioningProfile) log.Detail("- ForceCodeSignIdentity: %s", configs.ForceCodeSignIdentity) log.Detail("- CustomExportOptionsPlistContent:") if configs.CustomExportOptionsPlistContent != "" { log.Detail(configs.CustomExportOptionsPlistContent) } fmt.Println() log.Info("xcodebuild configs:") log.Detail("- OutputTool: %s", configs.OutputTool) log.Detail("- Workdir: %s", configs.Workdir) log.Detail("- ProjectPath: %s", configs.ProjectPath) log.Detail("- Scheme: %s", configs.Scheme) log.Detail("- Configuration: %s", configs.Configuration) log.Detail("- OutputDir: %s", configs.OutputDir) log.Detail("- IsCleanBuild: %s", configs.IsCleanBuild) log.Detail("- XcodebuildOptions: %s", configs.XcodebuildOptions) fmt.Println() log.Info("step output configs:") log.Detail("- IsExportXcarchiveZip: %s", configs.IsExportXcarchiveZip) log.Detail("- ExportAllDsyms: %s", configs.ExportAllDsyms) log.Detail("- ArtifactName: %s", configs.ArtifactName) fmt.Println() }
func registerFail(format string, v ...interface{}) { log.Error(format, v...) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) }
func main() { configs := createConfigsModelFromEnvs() fmt.Println() configs.print() if explanation, err := configs.validate(); err != nil { fmt.Println() log.Error("Issue with input: %s", err) fmt.Println() if explanation != "" { fmt.Println(explanation) fmt.Println() } os.Exit(1) } err := os.Chmod(configs.GradlewPath, 0770) if err != nil { log.Error("Failed to add executable permission on gradlew file (%s), error: %s", configs.GradlewPath, err) os.Exit(1) } fmt.Println() log.Info("Running gradle task...") if err := runGradleTask(configs.GradlewPath, configs.GradleFile, configs.UnitTestTasks, configs.UnitTestFlags); err != nil { log.Error("Gradle task failed, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_GRADLE_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_GRADLE_TEST_RESULT", err) } os.Exit(1) } if err := exportEnvironmentWithEnvman("BITRISE_GRADLE_TEST_RESULT", "succeeded"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_GRADLE_TEST_RESULT", err) } }
// GetSimulator ... func GetSimulator(simulatorPlatform, simulatorDevice, simulatorOsVersion string) (models.SimInfoModel, error) { cmd := exec.Command("xcrun", "simctl", "list") outBytes, err := cmd.CombinedOutput() if err != nil { return models.SimInfoModel{}, err } simctlListOut := string(outBytes) allSimIDsGroupedBySimVersion := collectAllSimIDs(simctlListOut) // // map desired inputs simulatorPlatformSplit := strings.Split(simulatorPlatform, " Simulator") if len(simulatorPlatformSplit) == 0 { return models.SimInfoModel{}, fmt.Errorf("failed to parse simulator platform (%s)", simulatorPlatform) } if simulatorDevice == "iPad" { log.Warn("Given device (%s) is deprecated, using (iPad 2)...", simulatorDevice) simulatorDevice = "iPad 2" } desiredPlatform := simulatorPlatformSplit[0] desiredOsVersion := "" if simulatorOsVersion == "latest" { latestOsVersion, err := getLatestOsVersion(desiredPlatform, simulatorDevice, allSimIDsGroupedBySimVersion) if err != nil { return models.SimInfoModel{}, fmt.Errorf("failed to get latest os version, error: %s", err) } desiredOsVersion = latestOsVersion } else { desiredOsVersion = fmt.Sprintf("%s %s", desiredPlatform, simulatorOsVersion) } // // find desired simulator simInfoList, found := allSimIDsGroupedBySimVersion[desiredOsVersion] if !found { return models.SimInfoModel{}, fmt.Errorf("no simulator found for desired os: %s", desiredOsVersion) } for _, simInfo := range simInfoList { if simInfo.Name == simulatorDevice { return simInfo, nil } } return models.SimInfoModel{}, fmt.Errorf("%s - %s - %s not found", simulatorPlatform, simulatorDevice, simulatorOsVersion) }
func saveAttachements(projectPath, scheme string) error { projectName := filepath.Base(projectPath) projectExt := filepath.Ext(projectName) projectName = strings.TrimSuffix(projectName, projectExt) userHome := pathutil.UserHomeDir() deviedDataDir := filepath.Join(userHome, "Library/Developer/Xcode/DerivedData") projectDerivedDataDirPattern := filepath.Join(deviedDataDir, fmt.Sprintf("%s-*", projectName)) projectDerivedDataDirs, err := filepath.Glob(projectDerivedDataDirPattern) if err != nil { return err } if len(projectDerivedDataDirs) > 1 { return fmt.Errorf("more than 1 project derived data dir found: %v, with pattern: %s", projectDerivedDataDirs, projectDerivedDataDirPattern) } else if len(projectDerivedDataDirs) == 0 { return fmt.Errorf("no project derived data dir found with pattern: %s", projectDerivedDataDirPattern) } projectDerivedDataDir := projectDerivedDataDirs[0] testLogDir := filepath.Join(projectDerivedDataDir, "Logs", "Test") if exist, err := pathutil.IsDirExists(testLogDir); err != nil { return err } else if !exist { return fmt.Errorf("no test logs found at: %s", projectDerivedDataDir) } testLogAttachmentsDir := filepath.Join(testLogDir, "Attachments") if exist, err := pathutil.IsDirExists(testLogAttachmentsDir); err != nil { return err } else if !exist { return fmt.Errorf("no test attachments found at: %s", testLogAttachmentsDir) } deployDir := os.Getenv("BITRISE_DEPLOY_DIR") if deployDir == "" { return errors.New("No BITRISE_DEPLOY_DIR found") } zipedTestsDerivedDataPath := filepath.Join(deployDir, fmt.Sprintf("%s-xc-test-Attachments.zip", scheme)) if err := cmd.Zip(testLogDir, "Attachments", zipedTestsDerivedDataPath); err != nil { return err } if err := cmd.ExportEnvironmentWithEnvman("BITRISE_XCODE_TEST_ATTACHMENTS_PATH", zipedTestsDerivedDataPath); err != nil { log.Warn("Failed to export: BITRISE_XCODE_TEST_ATTACHMENTS_PATH, error: %s", err) } return nil }
func main() { configs := createConfigsModelFromEnvs() fmt.Println() configs.print() if err := configs.validate(); err != nil { log.Error("Issue with input: %s", err) os.Exit(1) } nugetPth := "/Library/Frameworks/Mono.framework/Versions/Current/bin/nuget" nugetRestoreCmdArgs := []string{nugetPth} if configs.NugetVersion == "latest" { fmt.Println() log.Info("Updating Nuget to latest version...") // "sudo $nuget update -self" cmdArgs := []string{"sudo", nugetPth, "update", "-self"} cmd, err := cmdex.NewCommandFromSlice(cmdArgs) if err != nil { log.Error("Failed to create command from args (%v), error: %s", cmdArgs, err) os.Exit(1) } cmd.SetStdout(os.Stdout) cmd.SetStderr(os.Stderr) log.Done("$ %s", cmdex.PrintableCommandArgs(false, cmdArgs)) if err := cmd.Run(); err != nil { log.Error("Failed to update nuget, error: %s", err) os.Exit(1) } } else if configs.NugetVersion != "" { fmt.Println() log.Info("Downloading Nuget %s version...", configs.NugetVersion) tmpDir, err := pathutil.NormalizedOSTempDirPath("__nuget__") if err != nil { log.Error("Failed to create tmp dir, error: %s", err) os.Exit(1) } downloadPth := filepath.Join(tmpDir, "nuget.exe") // https://dist.nuget.org/win-x86-commandline/v3.3.0/nuget.exe nugetURL := fmt.Sprintf("https://dist.nuget.org/win-x86-commandline/v%s/nuget.exe", configs.NugetVersion) log.Detail("Download URL: %s", nugetURL) if err := DownloadFile(nugetURL, downloadPth); err != nil { log.Warn("Download failed, error: %s", err) // https://dist.nuget.org/win-x86-commandline/v3.4.4/NuGet.exe nugetURL = fmt.Sprintf("https://dist.nuget.org/win-x86-commandline/v%s/NuGet.exe", configs.NugetVersion) log.Detail("Retry download URl: %s", nugetURL) if err := DownloadFile(nugetURL, downloadPth); err != nil { log.Error("Failed to download nuget, error: %s", err) os.Exit(1) } } nugetRestoreCmdArgs = []string{constants.MonoPath, downloadPth} } fmt.Println() log.Info("Restoring Nuget packages...") nugetRestoreCmdArgs = append(nugetRestoreCmdArgs, "restore", configs.XamarinSolution) if err := retry.Times(1).Try(func(attempt uint) error { if attempt > 0 { log.Warn("Attempt %d failed, retrying...", attempt) } log.Done("$ %s", cmdex.PrintableCommandArgs(false, nugetRestoreCmdArgs)) cmd, err := cmdex.NewCommandFromSlice(nugetRestoreCmdArgs) if err != nil { log.Error("Failed to create Nuget command, error: %s", err) os.Exit(1) } cmd.SetStdout(os.Stdout) cmd.SetStderr(os.Stderr) if err := cmd.Run(); err != nil { log.Error("Restore failed, error: %s", err) return err } return nil }); err != nil { log.Error("Nuget restore failed, error: %s", err) os.Exit(1) } }
func runTest(buildTestParams models.XcodeBuildTestParamsModel, outputTool, xcprettyOptions string, isAutomaticRetryOnReason, isRetryOnFail bool) (string, int, error) { handleTestError := func(fullOutputStr string, exitCode int, testError error) (string, int, error) { // // Automatic retry for _, retryReasonPattern := range automaticRetryReasonPatterns { if isStringFoundInOutput(retryReasonPattern, fullOutputStr) { log.Warn("Automatic retry reason found in log: %s", retryReasonPattern) if isAutomaticRetryOnReason { log.Detail("isAutomaticRetryOnReason=true - retrying...") return runTest(buildTestParams, outputTool, xcprettyOptions, false, false) } log.Error("isAutomaticRetryOnReason=false, no more retry, stopping the test!") return fullOutputStr, exitCode, testError } } // // Retry on fail if isRetryOnFail { log.Warn("Test run failed") log.Detail("isRetryOnFail=true - retrying...") return runTest(buildTestParams, outputTool, xcprettyOptions, false, false) } return fullOutputStr, exitCode, testError } buildParams := buildTestParams.BuildParams xcodebuildArgs := []string{buildParams.Action, buildParams.ProjectPath, "-scheme", buildParams.Scheme} if buildTestParams.CleanBuild { xcodebuildArgs = append(xcodebuildArgs, "clean") } // the 'build' argument is required *before* the 'test' arg, to prevent // the Xcode bug described in the README, which causes: // 'iPhoneSimulator: Timed out waiting 120 seconds for simulator to boot, current state is 1.' // in case the compilation takes a long time. // Related Radar link: https://openradar.appspot.com/22413115 // Demonstration project: https://github.com/bitrise-io/simulator-launch-timeout-includes-build-time // for builds < 120 seconds or fixed Xcode versions, one should // have the possibility of opting out, because the explicit build arg // leads the project to be compiled twice and increase the duration // Related issue link: https://github.com/bitrise-io/steps-xcode-test/issues/55 if buildTestParams.BuildBeforeTest { xcodebuildArgs = append(xcodebuildArgs, "build") } xcodebuildArgs = append(xcodebuildArgs, "test", "-destination", buildParams.DeviceDestination) if buildTestParams.GenerateCodeCoverage { xcodebuildArgs = append(xcodebuildArgs, "GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES") xcodebuildArgs = append(xcodebuildArgs, "GCC_GENERATE_TEST_COVERAGE_FILES=YES") } if buildTestParams.AdditionalOptions != "" { options, err := shellquote.Split(buildTestParams.AdditionalOptions) if err != nil { return "", 1, fmt.Errorf("failed to parse additional options (%s), error: %s", buildTestParams.AdditionalOptions, err) } xcodebuildArgs = append(xcodebuildArgs, options...) } xcprettyArgs := []string{} if xcprettyOptions != "" { options, err := shellquote.Split(xcprettyOptions) if err != nil { return "", 1, fmt.Errorf("failed to parse additional options (%s), error: %s", xcprettyOptions, err) } // get and delete the xcpretty output file, if exists xcprettyOutputFilePath := "" isNextOptOutputPth := false for _, aOpt := range options { if isNextOptOutputPth { xcprettyOutputFilePath = aOpt break } if aOpt == "--output" { isNextOptOutputPth = true continue } } if xcprettyOutputFilePath != "" { if isExist, err := pathutil.IsPathExists(xcprettyOutputFilePath); err != nil { log.Error("Failed to check xcpretty output file status (path: %s), error: %s", xcprettyOutputFilePath, err) } else if isExist { log.Warn("=> Deleting existing xcpretty output: %s", xcprettyOutputFilePath) if err := os.Remove(xcprettyOutputFilePath); err != nil { log.Error("Failed to delete xcpretty output file (path: %s), error: %s", xcprettyOutputFilePath, err) } } } // xcprettyArgs = append(xcprettyArgs, options...) } log.Info("Running the tests...") var rawOutput string var err error var exit int if outputTool == "xcpretty" { rawOutput, exit, err = runPrettyXcodeBuildCmd(true, xcprettyArgs, xcodebuildArgs) } else { rawOutput, exit, err = runXcodeBuildCmd(true, xcodebuildArgs...) } if err != nil { return handleTestError(rawOutput, exit, err) } return rawOutput, exit, nil }
func runPrettyXcodeBuildCmd(useStdOut bool, xcprettyArgs []string, xcodebuildArgs []string) (string, int, error) { // buildCmd := cmd.CreateXcodebuildCmd(xcodebuildArgs...) prettyCmd := cmd.CreateXcprettyCmd(xcprettyArgs...) // var buildOutBuffer bytes.Buffer // pipeReader, pipeWriter := io.Pipe() // // build outputs: // - write it into a buffer // - write it into the pipe, which will be fed into xcpretty buildOutWriters := []io.Writer{pipeWriter} buildOutWriter := cmd.CreateBufferedWriter(&buildOutBuffer, buildOutWriters...) // var prettyOutWriter io.Writer if useStdOut { prettyOutWriter = os.Stdout } // and set the writers buildCmd.Stdin = nil buildCmd.Stdout = buildOutWriter buildCmd.Stderr = buildOutWriter // prettyCmd.Stdin = pipeReader prettyCmd.Stdout = prettyOutWriter prettyCmd.Stderr = prettyOutWriter // buildCmd.Env = append(os.Environ(), xcodeCommandEnvs...) log.Detail("$ set -o pipefail && %s | %v", cmd.PrintableCommandArgsWithEnvs(buildCmd.Args, xcodeCommandEnvs), cmd.PrintableCommandArgs(prettyCmd.Args)) fmt.Println() if err := buildCmd.Start(); err != nil { return buildOutBuffer.String(), 1, err } if err := prettyCmd.Start(); err != nil { return buildOutBuffer.String(), 1, err } defer func() { if err := pipeWriter.Close(); err != nil { log.Warn("Failed to close xcodebuild-xcpretty pipe, error: %s", err) } if err := prettyCmd.Wait(); err != nil { log.Warn("xcpretty command failed, error: %s", err) } }() if err := buildCmd.Wait(); err != nil { if exitError, ok := err.(*exec.ExitError); ok { waitStatus, ok := exitError.Sys().(syscall.WaitStatus) if !ok { return buildOutBuffer.String(), 1, errors.New("Failed to cast exit status") } return buildOutBuffer.String(), waitStatus.ExitStatus(), err } return buildOutBuffer.String(), 1, err } return buildOutBuffer.String(), 0, nil }
func main() { configs := createConfigsModelFromEnvs() fmt.Println() configs.print() if err := configs.validate(); err != nil { log.Error("Issue with input: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } // Get Simulator Infos fmt.Println() log.Info("Collecting simulator info...") simulatorInfo, err := getSimulatorInfo(configs.SimulatorOsVersion, configs.SimulatorDevice) if err != nil { log.Error("Failed to get simulator infos, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } log.Done("Simulator (%s), id: (%s), status: %s", simulatorInfo.Name, simulatorInfo.ID, simulatorInfo.Status) // --- // Nunit Console path nunitConsolePth, err := nunit.SystemNunit3ConsolePath() if err != nil { log.Error("Failed to get system insatlled nunit3-console.exe path, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } // --- // // build fmt.Println() log.Info("Building all iOS Xamarin UITest and Referred Projects in solution: %s", configs.XamarinSolution) builder, err := builder.New(configs.XamarinSolution, []constants.ProjectType{constants.ProjectTypeIOS}, false) if err != nil { log.Error("Failed to create xamarin builder, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } callback := func(solutionName string, projectName string, projectType constants.ProjectType, commandStr string, alreadyPerformed bool) { fmt.Println() if projectType == constants.ProjectTypeXamarinUITest { log.Info("Building test project: %s", projectName) } else { log.Info("Building project: %s", projectName) } log.Done("$ %s", commandStr) if alreadyPerformed { log.Warn("build command already performed, skipping...") } fmt.Println() } warnings, err := builder.BuildAllXamarinUITestAndReferredProjects(configs.XamarinConfiguration, configs.XamarinPlatform, nil, callback) for _, warning := range warnings { log.Warn(warning) } if err != nil { log.Error("Build failed, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } projectOutputMap, err := builder.CollectProjectOutputs(configs.XamarinConfiguration, configs.XamarinPlatform) if err != nil { log.Error("Failed to collect project outputs, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } testProjectOutputMap, warnings, err := builder.CollectXamarinUITestProjectOutputs(configs.XamarinConfiguration, configs.XamarinPlatform) for _, warning := range warnings { log.Warn(warning) } if err != nil { log.Error("Failed to collect test project output, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } // --- // // Run nunit tests nunitConsole, err := nunit.New(nunitConsolePth) if err != nil { log.Error("Failed to create nunit console model, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } resultLogPth := filepath.Join(configs.DeployDir, "TestResult.xml") nunitConsole.SetResultLogPth(resultLogPth) // Artifacts resultLog := "" for testProjectName, testProjectOutput := range testProjectOutputMap { if len(testProjectOutput.ReferredProjectNames) == 0 { log.Warn("Test project (%s) does not refers to any project, skipping...", testProjectName) continue } for _, projectName := range testProjectOutput.ReferredProjectNames { projectOutput, ok := projectOutputMap[projectName] if !ok { continue } appPth := "" for _, output := range projectOutput.Outputs { if output.OutputType == constants.OutputTypeAPP { appPth = output.Pth } } if appPth == "" { log.Error("No app generated for project: %s", projectName) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } // Set APP_BUNDLE_PATH env to let the test know which .app file should be tested // This env is used in the Xamarin.UITest project to refer to the .app path if err := os.Setenv("APP_BUNDLE_PATH", appPth); err != nil { log.Error("Failed to set APP_BUNDLE_PATH environment, without this env test will fail, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } // Run test fmt.Println() log.Info("Testing (%s) against (%s)", testProjectName, projectName) log.Detail("test dll: %s", testProjectOutput.Output.Pth) log.Detail("app: %s", appPth) nunitConsole.SetDLLPth(testProjectOutput.Output.Pth) nunitConsole.SetTestToRun(configs.TestToRun) fmt.Println() log.Info("Running Xamarin UITest") log.Done("$ %s", nunitConsole.PrintableCommand()) fmt.Println() err := nunitConsole.Run() testLog, readErr := testResultLogContent(resultLogPth) if readErr != nil { log.Warn("Failed to read test result, error: %s", readErr) } resultLog = testLog if err != nil { log.Error("Test failed, error: %s", err) if errorMsg, err := parseErrorFromResultLog(resultLog); err != nil { log.Warn("Failed to parse error message from result log, error: %s", err) } else if errorMsg != "" { log.Error("%s", errorMsg) } if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } if resultLog != "" { if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", resultLog); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", err) } } os.Exit(1) } } } if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "succeeded"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } if resultLog != "" { if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", resultLog); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", err) } } }
func main() { configs := createConfigsModelFromEnvs() fmt.Println() configs.print() if err := configs.validate(); err != nil { log.Error("Issue with input: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } // // build fmt.Println() log.Info("Building all iOS Xamarin UITest and Referred Projects in solution: %s", configs.XamarinSolution) builder, err := builder.New(configs.XamarinSolution, []constants.ProjectType{constants.ProjectTypeIOS}, false) if err != nil { log.Error("Failed to create xamarin builder, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } callback := func(solutionName string, projectName string, projectType constants.ProjectType, commandStr string, alreadyPerformed bool) { fmt.Println() if projectType == constants.ProjectTypeXamarinUITest { log.Info("Building test project: %s", projectName) } else { log.Info("Building project: %s", projectName) } log.Done("$ %s", commandStr) if alreadyPerformed { log.Warn("build command already performed, skipping...") } fmt.Println() } warnings, err := builder.BuildAllXamarinUITestAndReferredProjects(configs.XamarinConfiguration, configs.XamarinPlatform, nil, callback) for _, warning := range warnings { log.Warn(warning) } if err != nil { log.Error("Build failed, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } projectOutputMap, err := builder.CollectProjectOutputs(configs.XamarinConfiguration, configs.XamarinPlatform) if err != nil { log.Error("Failed to collect project outputs, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } testProjectOutputMap, warnings, err := builder.CollectXamarinUITestProjectOutputs(configs.XamarinConfiguration, configs.XamarinPlatform) for _, warning := range warnings { log.Warn(warning) } if err != nil { log.Error("Failed to collect test project output, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } // --- // // Test Cloud submit solutionDir := filepath.Dir(configs.XamarinSolution) pattern := filepath.Join(solutionDir, "packages/Xamarin.UITest.*/tools/test-cloud.exe") testClouds, err := filepath.Glob(pattern) if err != nil { log.Error("Failed to find test-cloud.exe path with pattern (%s), error: %s", pattern, err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } if len(testClouds) == 0 { if err != nil { log.Error("No test-cloud.exe found path with pattern (%s)", pattern) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } } testCloud, err := testcloud.NewModel(testClouds[0]) if err != nil { log.Error("Failed to create test cloud model, error: %s", err) os.Exit(1) } testCloud.SetAPIKey(configs.APIKey) testCloud.SetUser(configs.User) testCloud.SetDevices(configs.Devices) testCloud.SetIsAsyncJSON(configs.IsAsync == "yes") testCloud.SetSeries(configs.Series) // If test cloud runs in asnyc mode test result will not be saved into file resultLogPth := filepath.Join(configs.DeployDir, "TestResult.xml") if configs.IsAsync != "yes" { testCloud.SetNunitXMLPth(resultLogPth) } // Parallelization if configs.Parallelization != "none" { parallelization, err := testcloud.ParseParallelization(configs.Parallelization) if err != nil { log.Error("Failed to parse parallelization, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } testCloud.SetParallelization(parallelization) } // --- // Custom Options if configs.CustomOptions != "" { options, err := shellquote.Split(configs.CustomOptions) if err != nil { log.Error("Failed to split params (%s), error: %s", configs.CustomOptions, err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } testCloud.SetCustomOptions(options...) } // --- // Artifacts resultLog := "" for testProjectName, testProjectOutput := range testProjectOutputMap { if len(testProjectOutput.ReferredProjectNames) == 0 { log.Warn("Test project (%s) does not refers to any project, skipping...", testProjectName) continue } for _, projectName := range testProjectOutput.ReferredProjectNames { projectOutput, ok := projectOutputMap[projectName] if !ok { continue } ipaPth := "" dsymPth := "" for _, output := range projectOutput.Outputs { if output.OutputType == constants.OutputTypeIPA { ipaPth = output.Pth } if output.OutputType == constants.OutputTypeDSYM { dsymPth = output.Pth } } if ipaPth == "" { log.Warn("No ipa generated for project: %s", projectName) } if dsymPth == "" { log.Warn("No dsym generated for project: %s", projectName) } // Submit fmt.Println() log.Info("Testing (%s) against (%s)", testProjectName, projectName) log.Detail("test dll: %s", testProjectOutput.Output.Pth) log.Detail("ipa: %s", ipaPth) log.Detail("dsym: %s", dsymPth) testCloud.SetAssemblyDir(filepath.Dir(testProjectOutput.Output.Pth)) testCloud.SetIPAPth(ipaPth) testCloud.SetDSYMPth(dsymPth) fmt.Println() log.Info("Submitting:") log.Done("$ %s", testCloud.PrintableCommand()) lines := []string{} callback := func(line string) { log.Detail(line) lines = append(lines, line) } err := testCloud.Submit(callback) // If test cloud runs in asnyc mode test result will not be saved into file if configs.IsAsync != "yes" { testLog, logErr := testResultLogContent(resultLogPth) if logErr != nil { log.Warn("Failed to read test result, error: %s", logErr) } resultLog = testLog } if err != nil { log.Error("Submit failed, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } if resultLog != "" { if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", resultLog); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", err) } } os.Exit(1) } // --- if configs.IsAsync == "yes" { fmt.Println() log.Info("Preocessing json result:") jsonLine := "" for _, line := range lines { if strings.HasPrefix(line, "{") && strings.HasSuffix(line, "}") { jsonLine = line } } if jsonLine != "" { var result JSONResultModel if err := json.Unmarshal([]byte(jsonLine), &result); err != nil { log.Error("Failed to unmarshal result, error: %s", err) } else { for _, errorMsg := range result.ErrorMessages { log.Error(errorMsg) } if len(result.ErrorMessages) > 0 { if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } if resultLog != "" { if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", resultLog); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", err) } } os.Exit(1) } if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_TO_RUN_ID", result.TestRunID); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_TO_RUN_ID", err) } log.Done("TestRunId (%s) is available in (%s) environment variable", result.TestRunID, "BITRISE_XAMARIN_TEST_TO_RUN_ID") } } } } } // --- if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "succeeded"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } if resultLog != "" { if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", resultLog); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", err) } } }
func main() { configs := createConfigsModelFromEnvs() fmt.Println() configs.print() if err := configs.validate(); err != nil { registerFail("Issue with input: %s", err) } // // Determining calabash-android version fmt.Println() log.Info("Determining calabash-android version...") rubyCommand, err := rubycmd.NewRubyCommandModel() if err != nil { registerFail("Failed to create ruby command, err: %s", err) } calabashAndroidVersion := "" useBundler := false if configs.GemFilePath != "" { if exist, err := pathutil.IsPathExists(configs.GemFilePath); err != nil { registerFail("Failed to check if Gemfile exists at (%s) exist, error: %s", configs.GemFilePath, err) } else if exist { log.Detail("Gemfile exists at: %s", configs.GemFilePath) gemfileDir := filepath.Dir(configs.GemFilePath) gemfileLockPth := filepath.Join(gemfileDir, "Gemfile.lock") if exist, err := pathutil.IsPathExists(gemfileLockPth); err != nil { registerFail("Failed to check if Gemfile.lock exists at (%s), error: %s", gemfileLockPth, err) } else if exist { log.Detail("Gemfile.lock exists at: %s", gemfileLockPth) version, err := calabashAndroidVersionFromGemfileLock(gemfileLockPth) if err != nil { registerFail("Failed to get calabash-android version from Gemfile.lock, error: %s", err) } log.Detail("calabash-android version in Gemfile.lock: %s", version) calabashAndroidVersion = version useBundler = true } else { log.Warn("Gemfile.lock doest no find with calabash-android gem at: %s", gemfileLockPth) } } else { log.Warn("Gemfile doest no find with calabash-android gem at: %s", configs.GemFilePath) } } if configs.CalabashAndroidVersion != "" { log.Detail("calabash-android version in configs: %s", configs.CalabashAndroidVersion) calabashAndroidVersion = configs.CalabashAndroidVersion useBundler = false } if calabashAndroidVersion == "" { log.Done("using calabash-android latest version") } else { log.Done("using calabash-android version: %s", calabashAndroidVersion) } // --- // // Intsalling calabash-android gem fmt.Println() log.Info("Installing calabash-android gem...") calabashAndroidArgs := []string{} // If Gemfile given with calabash-android and calabash_android_version input does not override calabash-android version // Run `bundle install` // Run calabash-android with `bundle exec` if useBundler { bundleInstallArgs := []string{"bundle", "install", "--jobs", "20", "--retry", "5"} // bundle install bundleInstallCmd, err := rubyCommand.Command(false, bundleInstallArgs) if err != nil { registerFail("Failed to create command, error: %s", err) } bundleInstallCmd.AppendEnvs([]string{"BUNDLE_GEMFILE=" + configs.GemFilePath}) log.Detail("$ %s", cmdex.PrintableCommandArgs(false, bundleInstallArgs)) if err := bundleInstallCmd.Run(); err != nil { registerFail("bundle install failed, error: %s", err) } // --- calabashAndroidArgs = []string{"bundle", "exec"} } calabashAndroidArgs = append(calabashAndroidArgs, "calabash-android") // If no need to use bundler if !useBundler { if calabashAndroidVersion != "" { // ... and calabash-android version detected // Install calabash-android detetcted version with `gem install` // Append version param to calabash-android command installed, err := rubyCommand.IsGemInstalled("calabash-android", calabashAndroidVersion) if err != nil { registerFail("Failed to check if calabash-android (v%s) installed, error: %s", calabashAndroidVersion, err) } if !installed { installCommands, err := rubyCommand.GemInstallCommands("calabash-android", calabashAndroidVersion) if err != nil { registerFail("Failed to create gem install commands, error: %s", err) } for _, installCommand := range installCommands { log.Detail("$ %s", cmdex.PrintableCommandArgs(false, installCommand.GetCmd().Args)) if err := installCommand.Run(); err != nil { registerFail("command failed, error: %s", err) } } } else { log.Detail("calabash-android %s installed", calabashAndroidVersion) } } else { // ... and using latest version of calabash-android // Install calabash-android latest version with `gem install` installCommands, err := rubyCommand.GemInstallCommands("calabash-android", "") if err != nil { registerFail("Failed to create gem install commands, error: %s", err) } for _, installCommand := range installCommands { log.Detail("$ %s", cmdex.PrintableCommandArgs(false, installCommand.GetCmd().Args)) if err := installCommand.Run(); err != nil { registerFail("command failed, error: %s", err) } } } } // --- // // Search for debug.keystore fmt.Println() log.Info("Search for debug.keystore...") debugKeystorePth := "" homeDir := pathutil.UserHomeDir() // $HOME/.android/debug.keystore androidDebugKeystorePth := filepath.Join(homeDir, ".android", "debug.keystore") debugKeystorePth = androidDebugKeystorePth if exist, err := pathutil.IsPathExists(androidDebugKeystorePth); err != nil { registerFail("Failed to check if debug.keystore exists at (%s), error: %s", androidDebugKeystorePth, err) } else if !exist { log.Warn("android debug keystore not exist at: %s", androidDebugKeystorePth) // $HOME/.local/share/Mono for Android/debug.keystore xamarinDebugKeystorePth := filepath.Join(homeDir, ".local", "share", "Mono for Android", "debug.keystore") log.Detail("checking xamarin debug keystore at: %s", xamarinDebugKeystorePth) if exist, err := pathutil.IsPathExists(xamarinDebugKeystorePth); err != nil { registerFail("Failed to check if debug.keystore exists at (%s), error: %s", xamarinDebugKeystorePth, err) } else if !exist { log.Warn("xamarin debug keystore not exist at: %s", xamarinDebugKeystorePth) log.Detail("generating debug keystore") // `keytool -genkey -v -keystore "#{debug_keystore}" -alias androiddebugkey -storepass android -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug,O=Android,C=US"` keytoolArgs := []string{"keytool", "-genkey", "-v", "-keystore", debugKeystorePth, "-alias", "androiddebugkey", "-storepass", "android", "-keypass", "android", "-keyalg", "RSA", "-keysize", "2048", "-validity", "10000", "-dname", "CN=Android Debug,O=Android,C=US"} cmd, err := cmdex.NewCommandFromSlice(keytoolArgs) if err != nil { registerFail("Failed to create command, error: %s", err) } log.Detail("$ %s", cmdex.PrintableCommandArgs(false, keytoolArgs)) if err := cmd.Run(); err != nil { registerFail("Failed to generate debug.keystore, error: %s", err) } log.Detail("using debug keystore: %s", debugKeystorePth) } else { log.Detail("using xamarin debug keystore: %s", xamarinDebugKeystorePth) debugKeystorePth = xamarinDebugKeystorePth } } else { log.Detail("using android debug keystore: %s", androidDebugKeystorePth) } // --- // // Resign apk with debug.keystore fmt.Println() log.Info("Resign apk with debug.keystore...") resignArgs := []string{"calabash-android", "resign", configs.ApkPath} resignCmd, err := rubyCommand.Command(useBundler, resignArgs) if err != nil { registerFail("Failed to create command, error: %s", err) } log.Detail("$ %s", cmdex.PrintableCommandArgs(false, resignArgs)) fmt.Println() resignCmd.SetStdout(os.Stdout) resignCmd.SetStderr(os.Stderr) if err := resignCmd.Run(); err != nil { registerFail("Failed to run command, error: %s", err) } // --- // // Run calabash-android fmt.Println() log.Info("Running calabash-android test...") testArgs := []string{"calabash-android", "run", configs.ApkPath} testCmd, err := rubyCommand.Command(useBundler, testArgs) if err != nil { registerFail("Failed to create command, error: %s", err) } log.Detail("$ %s", cmdex.PrintableCommandArgs(false, testArgs)) fmt.Println() testCmd.SetStdout(os.Stdout) testCmd.SetStderr(os.Stderr) if err := testCmd.Run(); err != nil { registerFail("Failed to run command, error: %s", err) } // --- if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "succeeded"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } }
func main() { configs := createConfigsModelFromEnvs() configs.print() if err := configs.validate(); err != nil { log.Error("Issue with input: %s", err) os.Exit(1) } fmt.Println() // Validate Certificates certificateURLPassphraseMap := map[string]string{} if configs.CertificateURL != "" { certificateURLs := strings.Split(configs.CertificateURL, "|") certificatePassphrases := strings.Split(configs.CertificatePassphrase, "|") if len(certificateURLs) != len(certificatePassphrases) { log.Error("Certificate url count: (%d), not equals to Certificate Passphrase count: (%d)", len(certificateURLs), len(certificatePassphrases)) os.Exit(1) } for i := 0; i < len(certificateURLs); i++ { certificateURL := certificateURLs[i] certificatePassphrase := certificatePassphrases[i] certificateURLPassphraseMap[certificateURL] = certificatePassphrase } } if configs.DefaultCertificateURL != "" { log.Detail("Default Certificate given") certificateURLPassphraseMap[configs.DefaultCertificateURL] = configs.DefaultCertificatePassphrase } certificateCount := len(certificateURLPassphraseMap) log.Detail("Provided Certificate count: %d", certificateCount) if certificateCount == 0 { log.Error("No Certificate provided") os.Exit(1) } // Validate Provisioning Profiles provisioningProfileURLs := strings.Split(configs.ProvisioningProfileURL, "|") if configs.DefaultProvisioningProfileURL != "" { log.Detail("Default Provisioning Profile given") provisioningProfileURLs = append(provisioningProfileURLs, configs.DefaultProvisioningProfileURL) } profileCount := len(provisioningProfileURLs) log.Detail("Provided Provisioning Profile count: %d", profileCount) if profileCount == 0 { log.Error("No Provisioning Profile provided") os.Exit(1) } // // Init homeDir := os.Getenv("HOME") provisioningProfileDir := path.Join(homeDir, "Library/MobileDevice/Provisioning Profiles") if exist, err := pathutil.IsPathExists(provisioningProfileDir); err != nil { log.Error("Failed to check path (%s), err: %s", provisioningProfileDir, err) os.Exit(1) } else if !exist { if err := os.MkdirAll(provisioningProfileDir, 0777); err != nil { log.Error("Failed to create path (%s), err: %s", provisioningProfileDir, err) os.Exit(1) } } tempDir, err := pathutil.NormalizedOSTempDirPath("bitrise-cert-tmp") if err != nil { log.Error("Failed to create tmp directory, err: %s", err) os.Exit(1) } if exist, err := pathutil.IsPathExists(configs.KeychainPath); err != nil { log.Error("Failed to check path (%s), err: %s", configs.KeychainPath, err) os.Exit(1) } else if !exist { fmt.Println() log.Warn("Keychain (%s) does not exist", configs.KeychainPath) keychainPth := fmt.Sprintf("%s-db", configs.KeychainPath) log.Detail(" Checking (%s)", keychainPth) if exist, err := pathutil.IsPathExists(keychainPth); err != nil { log.Error("Failed to check path (%s), err: %s", keychainPth, err) os.Exit(1) } else if !exist { log.Info("Creating keychain: %s", configs.KeychainPath) if out, err := runCommandAndReturnCombinedStdoutAndStderr("security", "-v", "create-keychain", "-p", configs.KeychainPassword, configs.KeychainPath); err != nil { log.Error("Failed to create keychain, output: %s", out) log.Error("Failed to create keychain, err: %s", err) os.Exit(1) } } } else { log.Detail("Keychain already exists, using it: %s", configs.KeychainPath) } // // Download certificate fmt.Println() log.Info("Downloading & installing Certificate(s)") certificatePassphraseMap := map[string]string{} idx := 0 for certURL, pass := range certificateURLPassphraseMap { fmt.Println() log.Detail("=> Downloading certificate: %d/%d", idx+1, certificateCount) certPath := path.Join(tempDir, fmt.Sprintf("Certificate-%d.p12", idx)) if err := downloadFile(certPath, certURL); err != nil { log.Error("Download failed, err: %s", err) os.Exit(1) } certificatePassphraseMap[certPath] = pass idx++ } // // Install certificate fmt.Println() log.Detail("=> Installing downloaded certificate") for cert, pass := range certificatePassphraseMap { // Import items into a keychain. importOut, err := runCommandAndReturnCombinedStdoutAndStderr("security", "import", cert, "-k", configs.KeychainPath, "-P", pass, "-A") if err != nil { log.Error("Command failed, output: %s", importOut) log.Error("Command failed, err: %s", err) os.Exit(1) } } // This is new behavior in Sierra, [openradar](https://openradar.appspot.com/28524119) // You need to use "security set-key-partition-list -S apple-tool:,apple: -k keychainPass keychainName" after importing the item and before attempting to use it via codesign. osVersionCmd := cmdex.NewCommand("sw_vers", "-productVersion") out, err := osVersionCmd.RunAndReturnTrimmedCombinedOutput() if err != nil { log.Error("Failed to get os version, error: %s", err) os.Exit(1) } osVersion, err := version.NewVersion(out) if err != nil { log.Error("Failed to parse os version (%s), error: %s", out, err) os.Exit(1) } sierraVersionStr := "10.12.0" sierraVersion, err := version.NewVersion(sierraVersionStr) if err != nil { log.Error("Failed to parse os version (%s), error: %s", sierraVersionStr, err) os.Exit(1) } if !osVersion.LessThan(sierraVersion) { cmd := cmdex.NewCommand("security", "set-key-partition-list", "-S", "apple-tool:,apple:", "-k", configs.KeychainPassword, configs.KeychainPath) if err := cmd.Run(); err != nil { log.Error("Failed, err: %s", err) os.Exit(1) } } // --- // Set keychain settings: Lock keychain when the system sleeps, Lock keychain after timeout interval, Timeout in seconds settingsOut, err := runCommandAndReturnCombinedStdoutAndStderr("security", "-v", "set-keychain-settings", "-lut", "72000", configs.KeychainPath) if err != nil { log.Error("Command failed, output: %s", settingsOut) log.Error("Command failed, err: %s", err) os.Exit(1) } // List keychains listKeychainsOut, err := runCommandAndReturnCombinedStdoutAndStderr("security", "list-keychains") if err != nil { log.Error("Command failed, output: %s", listKeychainsOut) log.Error("Command failed, err: %s", err) os.Exit(1) } keychainList := strings.Split(listKeychainsOut, "\n") strippedKeychainList := []string{} for _, keychain := range keychainList { strippedKeychain := strip(keychain) strippedKeychainList = append(strippedKeychainList, strippedKeychain) } strippedKeychainList = addKeyChainToList(strippedKeychainList, configs.KeychainPath) // Set keychain search path args := []string{"-v", "list-keychains", "-s"} args = append(args, strippedKeychainList...) listKeychainsOut, err = runCommandAndReturnCombinedStdoutAndStderr("security", args...) if err != nil { log.Error("Command failed, output: %s", listKeychainsOut) log.Error("Command failed, err: %s", err) os.Exit(1) } // Set the default keychain defaultKeychainOut, err := runCommandAndReturnCombinedStdoutAndStderr("security", "-v", "default-keychain", "-s", configs.KeychainPath) if err != nil { log.Error("Command failed, output: %s", defaultKeychainOut) log.Error("Command failed, err: %s", err) os.Exit(1) } // Unlock the specified keychain unlockOut, err := runCommandAndReturnCombinedStdoutAndStderr("security", "-v", "unlock-keychain", "-p", configs.KeychainPassword, configs.KeychainPath) if err != nil { log.Error("Command failed, output: %s", unlockOut) log.Error("Command failed, err: %s", err) os.Exit(1) } for cert, pass := range certificatePassphraseMap { certificateIdentity, err := certificateFriendlyName(cert, pass) if err != nil { log.Error("Failed to get cert identity, output: %s", certificateIdentity) log.Error("Failed to get cert identity, err: %s", err) os.Exit(1) } if certificateIdentity == "" { log.Error("Failed to get cert identity") os.Exit(1) } log.Done(" Installed certificate: %s", certificateIdentity) } certs, err := availableCertificates(configs.KeychainPath) if err != nil { log.Error("Failed to get certificate list, err:%s", err) os.Exit(1) } if len(certs) == 0 { log.Error("Failed to import certificate, no certificates found") os.Exit(1) } fmt.Println() log.Info("Available certificates:") fmt.Println("-----------------------") for _, cert := range certs { log.Detail(" * %s", cert) } // // Install provisioning profiles // NOTE: the URL can be a pipe (|) separated list of Provisioning Profile URLs fmt.Println() log.Info("Downloading & installing Provisioning Profile(s)") for idx, profileURL := range provisioningProfileURLs { fmt.Println() log.Detail("=> Downloading provisioning profile: %d/%d", idx+1, profileCount) provisioningProfileExt := "provisionprofile" if !strings.Contains(profileURL, "."+provisioningProfileExt) { provisioningProfileExt = "mobileprovision" } profileTmpPth := path.Join(tempDir, fmt.Sprintf("profile-%d.%s", idx, provisioningProfileExt)) if err := downloadFile(profileTmpPth, profileURL); err != nil { log.Error("Download failed, err: %s", err) os.Exit(1) } fmt.Println() fmt.Println("=> Installing provisioning profile") out, err := runCommandAndReturnCombinedStdoutAndStderr("/usr/bin/security", "cms", "-D", "-i", profileTmpPth) if err != nil { log.Error("Command failed, output: %s", out) log.Error("Command failed, err: %s", err) os.Exit(1) } outSplit := strings.Split(out, "\n") if len(outSplit) > 0 { if strings.Contains(outSplit[0], notValidParameterErrorMessage) { fixedOutSplit := outSplit[1:len(outSplit)] out = strings.Join(fixedOutSplit, "\n") } } tmpProvProfilePth := path.Join(tempDir, "prov") if err := writeBytesToFileWithPermission(tmpProvProfilePth, []byte(out), 0); err != nil { log.Error("Failed to write profile to file, error: %s", err) os.Exit(1) } profileInfos, err := printableProfileInfos(out) if err != nil { log.Error("Failed to read profile infos, err: %s", err) os.Exit(1) } fmt.Println() log.Info("Profile Infos:") log.Detail("%s", profileInfos) fmt.Println() profileUUID, err := runCommandAndReturnCombinedStdoutAndStderr("/usr/libexec/PlistBuddy", "-c", "Print UUID", tmpProvProfilePth) if err != nil { log.Error("Command failed, output: %s", profileUUID) log.Error("Command failed, err: %s", err) os.Exit(1) } log.Done(" Installed Profile UUID: %s", profileUUID) profileFinalPth := path.Join(provisioningProfileDir, profileUUID+"."+provisioningProfileExt) log.Detail(" Moving it to: %s", profileFinalPth) if out, err := runCommandAndReturnCombinedStdoutAndStderr("cp", profileTmpPth, profileFinalPth); err != nil { log.Error("Command failed, output: %s", out) log.Error("Command failed, err: %s", err) os.Exit(1) } } }
func main() { configs := createConfigsModelFromEnvs() configs.print() if err := configs.validate(); err != nil { log.Error("Issue with input: %s", err) os.Exit(1) } fmt.Println() log.Info("Other Configs:") cleanBuild := (configs.IsCleanBuild == "yes") generateCodeCoverage := (configs.GenerateCodeCoverageFiles == "yes") exportUITestArtifacts := (configs.ExportUITestArtifacts == "true") singleBuild := (configs.IsSingleBuild == "true") buildBeforeTest := (configs.ShouldBuildBeforeTest == "yes") retryOnFail := (configs.ShouldRetryTestOnFail == "yes") // Project-or-Workspace flag action := "" if strings.HasSuffix(configs.ProjectPath, ".xcodeproj") { action = "-project" } else if strings.HasSuffix(configs.ProjectPath, ".xcworkspace") { action = "-workspace" } else { log.Error("Iinvalid project file (%s), extension should be (.xcodeproj/.xcworkspace)", configs.ProjectPath) if err := cmd.ExportEnvironmentWithEnvman("BITRISE_XCODE_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export: BITRISE_XCODE_TEST_RESULT, error: %s", err) } os.Exit(1) } log.Detail("* action: %s", action) // Device Destination deviceDestination := fmt.Sprintf("platform=%s,name=%s,OS=%s", configs.SimulatorPlatform, configs.SimulatorDevice, configs.SimulatorOsVersion) log.Detail("* device_destination: %s", deviceDestination) // Output tools versions xcodebuildVersion, err := xcodeutil.GetXcodeVersion() if err != nil { log.Error("Failed to get the version of xcodebuild! Error: %s", err) if err := cmd.ExportEnvironmentWithEnvman("BITRISE_XCODE_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export: BITRISE_XCODE_TEST_RESULT, error: %s", err) } os.Exit(1) } log.Detail("* xcodebuild_version: %s (%s)", xcodebuildVersion.Version, xcodebuildVersion.BuildVersion) xcprettyVersion, err := cmd.GetXcprettyVersion() if err != nil { log.Warn("Failed to get the xcpretty version! Error: %s", err) } else { log.Detail("* xcpretty_version: %s", xcprettyVersion) } // Simulator infos simulator, err := xcodeutil.GetSimulator(configs.SimulatorPlatform, configs.SimulatorDevice, configs.SimulatorOsVersion) if err != nil { log.Error(fmt.Sprintf("failed to get simulator udid, error: %s", err)) if err := cmd.ExportEnvironmentWithEnvman("BITRISE_XCODE_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export: BITRISE_XCODE_TEST_RESULT, error: %s", err) } os.Exit(1) } log.Detail("* simulator_name: %s, UDID: %s, status: %s", simulator.Name, simulator.SimID, simulator.Status) fmt.Println() buildParams := models.XcodeBuildParamsModel{ Action: action, ProjectPath: configs.ProjectPath, Scheme: configs.Scheme, DeviceDestination: deviceDestination, CleanBuild: cleanBuild, } buildTestParams := models.XcodeBuildTestParamsModel{ BuildParams: buildParams, BuildBeforeTest: buildBeforeTest, AdditionalOptions: configs.TestOptions, GenerateCodeCoverage: generateCodeCoverage, } if singleBuild { buildTestParams.CleanBuild = cleanBuild } // // Start simulator if simulator.Status == "Shutdown" { log.Info("Booting simulator (%s)...", simulator.SimID) if err := xcodeutil.BootSimulator(simulator, xcodebuildVersion); err != nil { log.Error(fmt.Sprintf("failed to boot simulator, error: %s", err)) if err := cmd.ExportEnvironmentWithEnvman("BITRISE_XCODE_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export: BITRISE_XCODE_TEST_RESULT, error: %s", err) } os.Exit(1) } if configs.WaitForSimulatorBoot == "yes" { log.Detail("Waiting for simulator boot") progress.SimpleProgress(".", 1*time.Second, func() { time.Sleep(60 * time.Second) }) } fmt.Println() } // // Run build if !singleBuild { if rawXcodebuildOutput, exitCode, buildErr := runBuild(buildParams, configs.OutputTool); buildErr != nil { if err := saveRawOutputToLogFile(rawXcodebuildOutput, false); err != nil { log.Warn("Failed to save the Raw Output, err: %s", err) } log.Warn("xcode build exit code: %d", exitCode) log.Warn("xcode build log:\n%s", rawXcodebuildOutput) log.Error("xcode build failed with error: %s", buildErr) if err := cmd.ExportEnvironmentWithEnvman("BITRISE_XCODE_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export: BITRISE_XCODE_TEST_RESULT, error: %s", err) } os.Exit(1) } } // // Run test rawXcodebuildOutput, exitCode, testErr := runTest(buildTestParams, configs.OutputTool, configs.XcprettyTestOptions, true, retryOnFail) if err := saveRawOutputToLogFile(rawXcodebuildOutput, (testErr == nil)); err != nil { log.Warn("Failed to save the Raw Output, error %s", err) } if exportUITestArtifacts { if err := saveAttachements(configs.ProjectPath, configs.Scheme); err != nil { log.Warn("Failed to export UI test artifacts, error %s", err) } } if testErr != nil { log.Warn("xcode test exit code: %d", exitCode) log.Error("xcode test failed, error: %s", testErr) hint := `If you can't find the reason of the error in the log, please check the raw-xcodebuild-output.log The log file is stored in $BITRISE_DEPLOY_DIR, and its full path is available in the $BITRISE_XCODE_RAW_TEST_RESULT_TEXT_PATH environment variable` log.Warn(hint) if err := cmd.ExportEnvironmentWithEnvman("BITRISE_XCODE_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export: BITRISE_XCODE_TEST_RESULT, error: %s", err) } os.Exit(1) } if err := cmd.ExportEnvironmentWithEnvman("BITRISE_XCODE_TEST_RESULT", "succeeded"); err != nil { log.Warn("Failed to export: BITRISE_XCODE_TEST_RESULT, error: %s", err) } }
func runCommandInDiagnosticMode(command cmdex.CommandModel, checkPattern string, waitTime time.Duration, forceWaitTime time.Duration, retryOnHang bool) error { log.Warn("Run in diagnostic mode") // copy command model to avoid re-run error: Stdout already set cmd := *command.GetCmd() timeout := false // Create a timer that will FORCE kill the process if normal kill does not work var forceKillError error var forceKillTimeoutHandler *time.Timer startForceKillTimeoutHandler := func() { forceKillTimeoutHandler = time.AfterFunc(forceWaitTime, func() { log.Warn("Process QUIT timeout") forceKillError = cmd.Process.Signal(syscall.SIGKILL) }) } // ---- // Create a timer that will kill the process var killError error var killTimeoutHandler *time.Timer startKillTimeoutHandler := func() { killTimeoutHandler = time.AfterFunc(waitTime, func() { log.Warn("Process timed out") timeout = true killError = cmd.Process.Signal(syscall.SIGQUIT) startForceKillTimeoutHandler() }) } // ---- // Redirect output stdoutReader, err := cmd.StdoutPipe() if err != nil { return err } scanner := bufio.NewScanner(stdoutReader) go func() { for scanner.Scan() { line := scanner.Text() fmt.Println(line) // stop timeout handler if new line comes if killTimeoutHandler != nil { killTimeoutHandler.Stop() } // if line contains check pattern start hang timeout handler if strings.Contains(strings.TrimSpace(line), checkPattern) { startKillTimeoutHandler() } } }() if err := scanner.Err(); err != nil { return err } // ---- if err := cmd.Start(); err != nil { return err } // Only proceed once the process has finished cmdErr := cmd.Wait() if killTimeoutHandler != nil { killTimeoutHandler.Stop() } if forceKillTimeoutHandler != nil { forceKillTimeoutHandler.Stop() } if cmdErr != nil { if !timeout || cmdErr.Error() != "signal: killed" { return cmdErr } } if killError != nil { return killError } if forceKillError != nil { return forceKillError } if timeout { if retryOnHang { return runCommandInDiagnosticMode(command, checkPattern, waitTime, forceWaitTime, false) } return fmt.Errorf("timed out") } return nil // ---- }
func main() { configs := createConfigsModelFromEnvs() fmt.Println() configs.print() if err := configs.validate(); err != nil { log.Error("Issue with input: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } // Custom Options resultLogPth := filepath.Join(configs.DeployDir, "TestResult.xml") customOptions := []string{"--result", resultLogPth} if configs.CustomOptions != "" { options, err := shellquote.Split(configs.CustomOptions) if err != nil { log.Error("Failed to split params (%s), error: %s", configs.CustomOptions, err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } customOptions = append(customOptions, options...) } // --- // // build fmt.Println() log.Info("Runing all nunit test projects in solution: %s", configs.XamarinSolution) builder, err := builder.New(configs.XamarinSolution, []constants.ProjectType{}, false) if err != nil { log.Error("Failed to create xamarin builder, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } os.Exit(1) } prepareCallback := func(solutionName string, projectName string, projectType constants.ProjectType, command *tools.Editable) { if projectType == constants.ProjectTypeNunitTest { (*command).SetCustomOptions(customOptions...) } } callback := func(solutionName string, projectName string, projectType constants.ProjectType, commandStr string, alreadyPerformed bool) { fmt.Println() if projectName == "" { log.Info("Building solution: %s", solutionName) } else { if projectType == constants.ProjectTypeNunitTest { log.Info("Building test project: %s", projectName) } else { log.Info("Building project: %s", projectName) } } log.Done("$ %s", commandStr) if alreadyPerformed { log.Warn("build command already performed, skipping...") } fmt.Println() } warnings, err := builder.BuildAllNunitTestProjects(configs.XamarinConfiguration, configs.XamarinPlatform, prepareCallback, callback) resultLog, logErr := testResultLogContent(resultLogPth) if logErr != nil { log.Warn("Failed to read test result, error: %s", logErr) } for _, warning := range warnings { log.Warn(warning) } if err != nil { log.Error("Test run failed, error: %s", err) if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "failed"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } if resultLog != "" { if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", resultLog); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", err) } } os.Exit(1) } if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_RESULT", "succeeded"); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_RESULT", err) } if resultLog != "" { if err := exportEnvironmentWithEnvman("BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", resultLog); err != nil { log.Warn("Failed to export environment: %s, error: %s", "BITRISE_XAMARIN_TEST_FULL_RESULTS_TEXT", err) } } }
func main() { configs := createConfigsModelFromEnvs() fmt.Println() configs.print() if err := configs.validate(); err != nil { fail("Issue with input: %s", err) } log.Info("step determined configs:") // Detect Xcode major version xcodebuildVersion, err := utils.XcodeBuildVersion() if err != nil { fail("Failed to determin xcode version, error: %s", err) } log.Detail("- xcodebuildVersion: %s (%s)", xcodebuildVersion.XcodeVersion.String(), xcodebuildVersion.BuildVersion) xcodeMajorVersion := xcodebuildVersion.XcodeVersion.Segments()[0] if xcodeMajorVersion < minSupportedXcodeMajorVersion { fail("Invalid xcode major version (%s), should not be less then min supported: %d", xcodeMajorVersion, minSupportedXcodeMajorVersion) } // Detect xcpretty version if configs.OutputTool == "xcpretty" { if !utils.IsXcprettyInstalled() { fail(`xcpretty is not installed For xcpretty installation see: 'https://github.com/supermarin/xcpretty', or use 'xcodebuild' as 'output_tool'.`) } xcprettyVersion, err := utils.XcprettyVersion() if err != nil { fail("Failed to determin xcpretty version, error: %s", err) } log.Detail("- xcprettyVersion: %s", xcprettyVersion.String()) } // Validation CustomExportOptionsPlistContent if configs.CustomExportOptionsPlistContent != "" && xcodeMajorVersion < 7 { log.Warn("CustomExportOptionsPlistContent is set, but CustomExportOptionsPlistContent only used if xcodeMajorVersion > 6") configs.CustomExportOptionsPlistContent = "" } if configs.ForceProvisioningProfileSpecifier != "" && xcodeMajorVersion < 8 { log.Warn("ForceProvisioningProfileSpecifier is set, but ForceProvisioningProfileSpecifier only used if xcodeMajorVersion > 7") configs.ForceProvisioningProfileSpecifier = "" } if configs.ForceTeamID == "" && xcodeMajorVersion < 8 { log.Warn("ForceTeamID is set, but ForceTeamID only used if xcodeMajorVersion > 7") configs.ForceTeamID = "" } if configs.ForceProvisioningProfileSpecifier != "" && configs.ForceProvisioningProfile != "" { log.Warn("both ForceProvisioningProfileSpecifier and ForceProvisioningProfile are set, using ForceProvisioningProfileSpecifier") configs.ForceProvisioningProfile = "" } fmt.Println() // abs out dir pth absOutputDir, err := pathutil.AbsPath(configs.OutputDir) if err != nil { fail("Failed to expand OutputDir (%s), error: %s", configs.OutputDir, err) } configs.OutputDir = absOutputDir if exist, err := pathutil.IsPathExists(configs.OutputDir); err != nil { fail("Failed to check if OutputDir exist, error: %s", err) } else if !exist { if err := os.MkdirAll(configs.OutputDir, 0777); err != nil { fail("Failed to create OutputDir (%s), error: %s", configs.OutputDir, err) } } // output files tmpArchiveDir, err := pathutil.NormalizedOSTempDirPath("__archive__") if err != nil { fail("Failed to create temp dir for archives, error: %s", err) } tmpArchivePath := filepath.Join(tmpArchiveDir, configs.ArtifactName+".xcarchive") appPath := filepath.Join(configs.OutputDir, configs.ArtifactName+".app") ipaPath := filepath.Join(configs.OutputDir, configs.ArtifactName+".ipa") exportOptionsPath := filepath.Join(configs.OutputDir, "export_options.plist") rawXcodebuildOutputLogPath := filepath.Join(configs.OutputDir, "raw-xcodebuild-output.log") dsymZipPath := filepath.Join(configs.OutputDir, configs.ArtifactName+".dSYM.zip") archiveZipPath := filepath.Join(configs.OutputDir, configs.ArtifactName+".xcarchive.zip") ideDistributionLogsZipPath := filepath.Join(configs.OutputDir, "xcodebuild.xcdistributionlogs.zip") // cleanup filesToCleanup := []string{ appPath, ipaPath, exportOptionsPath, rawXcodebuildOutputLogPath, dsymZipPath, archiveZipPath, ideDistributionLogsZipPath, } for _, pth := range filesToCleanup { if exist, err := pathutil.IsPathExists(pth); err != nil { fail("Failed to check if path (%s) exist, error: %s", pth, err) } else if exist { if err := os.RemoveAll(pth); err != nil { fail("Failed to remove path (%s), error: %s", pth, err) } } } // // Create the Archive with Xcode Command Line tools log.Info("Create the Archive ...") fmt.Println() isWorkspace := false ext := filepath.Ext(configs.ProjectPath) if ext == ".xcodeproj" { isWorkspace = false } else if ext == ".xcworkspace" { isWorkspace = true } else { fail("Project file extension should be .xcodeproj or .xcworkspace, but got: %s", ext) } archiveCmd := xcodebuild.NewArchiveCommand(configs.ProjectPath, isWorkspace) archiveCmd.SetScheme(configs.Scheme) archiveCmd.SetConfiguration(configs.Configuration) if configs.ForceTeamID != "" { log.Detail("Forcing Development Team: %s", configs.ForceTeamID) archiveCmd.SetForceDevelopmentTeam(configs.ForceTeamID) } if configs.ForceProvisioningProfileSpecifier != "" { log.Detail("Forcing Provisioning Profile Specifier: %s", configs.ForceProvisioningProfileSpecifier) archiveCmd.SetForceProvisioningProfileSpecifier(configs.ForceProvisioningProfileSpecifier) } if configs.ForceProvisioningProfile != "" { log.Detail("Forcing Provisioning Profile: %s", configs.ForceProvisioningProfile) archiveCmd.SetForceProvisioningProfile(configs.ForceProvisioningProfile) } if configs.ForceCodeSignIdentity != "" { log.Detail("Forcing Code Signing Identity: %s", configs.ForceCodeSignIdentity) archiveCmd.SetForceCodeSignIdentity(configs.ForceCodeSignIdentity) } if configs.IsCleanBuild == "yes" { archiveCmd.SetCustomBuildAction("clean") } archiveCmd.SetArchivePath(tmpArchivePath) if configs.XcodebuildOptions != "" { options, err := shellquote.Split(configs.XcodebuildOptions) if err != nil { fail("Failed to shell split XcodebuildOptions (%s), error: %s", configs.XcodebuildOptions) } archiveCmd.SetCustomOptions(options) } if configs.OutputTool == "xcpretty" { xcprettyCmd := xcpretty.New(archiveCmd) logWithTimestamp(colorstring.Green, "$ %s", xcprettyCmd.PrintableCmd()) fmt.Println() if rawXcodebuildOut, err := xcprettyCmd.Run(); err != nil { if err := utils.ExportOutputFileContent(rawXcodebuildOut, rawXcodebuildOutputLogPath, bitriseXcodeRawResultTextEnvKey); err != nil { log.Warn("Failed to export %s, error: %s", bitriseXcodeRawResultTextEnvKey, err) } else { log.Warn(`If you can't find the reason of the error in the log, please check the raw-xcodebuild-output.log The log file is stored in $BITRISE_DEPLOY_DIR, and its full path is available in the $BITRISE_XCODE_RAW_RESULT_TEXT_PATH environment variable`) } fail("Archive failed, error: %s", err) } } else { logWithTimestamp(colorstring.Green, "$ %s", archiveCmd.PrintableCmd()) fmt.Println() if err := archiveCmd.Run(); err != nil { fail("Archive failed, error: %s", err) } } fmt.Println() // Ensure xcarchive exists if exist, err := pathutil.IsPathExists(tmpArchivePath); err != nil { fail("Failed to check if archive exist, error: %s", err) } else if !exist { fail("No archive generated at: %s", tmpArchivePath) } // // Exporting the ipa with Xcode Command Line tools /* You'll get a "Error Domain=IDEDistributionErrorDomain Code=14 "No applicable devices found."" error if $GEM_HOME is set and the project's directory includes a Gemfile - to fix this we'll unset GEM_HOME as that's not required for xcodebuild anyway. This probably fixes the RVM issue too, but that still should be tested. See also: - http://stackoverflow.com/questions/33041109/xcodebuild-no-applicable-devices-found-when-exporting-archive - https://gist.github.com/claybridges/cea5d4afd24eda268164 */ log.Info("Exporting ipa from the archive...") fmt.Println() envsToUnset := []string{"GEM_HOME", "GEM_PATH", "RUBYLIB", "RUBYOPT", "BUNDLE_BIN_PATH", "_ORIGINAL_GEM_PATH", "BUNDLE_GEMFILE"} for _, key := range envsToUnset { if err := os.Unsetenv(key); err != nil { fail("Failed to unset (%s), error: %s", key, err) } } if xcodeMajorVersion == 6 || configs.UseDeprecatedExport == "yes" { log.Detail("Using legacy export") /* Get the name of the profile which was used for creating the archive --> Search for embedded.mobileprovision in the xcarchive. It should contain a .app folder in the xcarchive folder under the Products/Applications folder */ embeddedProfilePth, err := xcarchive.EmbeddedMobileProvisionPth(tmpArchivePath) if err != nil { fail("Failed to get embedded profile path, error: %s", err) } provProfile, err := provisioningprofile.NewFromFile(embeddedProfilePth) if err != nil { fail("Failed to create provisioning profile model, error: %s", err) } if provProfile.Name == nil { fail("Profile name empty") } legacyExportCmd := xcodebuild.NewLegacyExportCommand() legacyExportCmd.SetExportFormat("ipa") legacyExportCmd.SetArchivePath(tmpArchivePath) legacyExportCmd.SetExportPath(ipaPath) legacyExportCmd.SetExportProvisioningProfileName(*provProfile.Name) if configs.OutputTool == "xcpretty" { xcprettyCmd := xcpretty.New(legacyExportCmd) logWithTimestamp(colorstring.Green, xcprettyCmd.PrintableCmd()) fmt.Println() if rawXcodebuildOut, err := xcprettyCmd.Run(); err != nil { if err := utils.ExportOutputFileContent(rawXcodebuildOut, rawXcodebuildOutputLogPath, bitriseXcodeRawResultTextEnvKey); err != nil { log.Warn("Failed to export %s, error: %s", bitriseXcodeRawResultTextEnvKey, err) } else { log.Warn(`If you can't find the reason of the error in the log, please check the raw-xcodebuild-output.log The log file is stored in $BITRISE_DEPLOY_DIR, and its full path is available in the $BITRISE_XCODE_RAW_RESULT_TEXT_PATH environment variable`) } fail("Export failed, error: %s", err) } } else { logWithTimestamp(colorstring.Green, legacyExportCmd.PrintableCmd()) fmt.Println() if err := legacyExportCmd.Run(); err != nil { fail("Export failed, error: %s", err) } } } else { log.Detail("Using export options") if configs.CustomExportOptionsPlistContent != "" { log.Detail("Custom export options content provided:") fmt.Println(configs.CustomExportOptionsPlistContent) if err := fileutil.WriteStringToFile(exportOptionsPath, configs.CustomExportOptionsPlistContent); err != nil { fail("Failed to write export options to file, error: %s", err) } } else { log.Detail("Generating export options") var method exportoptions.Method if configs.ExportMethod == "auto-detect" { log.Detail("auto-detect export method, based on embedded profile") embeddedProfilePth, err := xcarchive.EmbeddedMobileProvisionPth(tmpArchivePath) if err != nil { fail("Failed to get embedded profile path, error: %s", err) } provProfile, err := provisioningprofile.NewFromFile(embeddedProfilePth) if err != nil { fail("Failed to create provisioning profile model, error: %s", err) } method = provProfile.GetExportMethod() log.Detail("detected export method: %s", method) } else { log.Detail("using export-method input: %s", configs.ExportMethod) parsedMethod, err := exportoptions.ParseMethod(configs.ExportMethod) if err != nil { fail("Failed to parse export options, error: %s", err) } method = parsedMethod } var exportOpts exportoptions.ExportOptions if method == exportoptions.MethodAppStore { options := exportoptions.NewAppStoreOptions() options.UploadBitcode = (configs.UploadBitcode == "yes") options.TeamID = configs.TeamID exportOpts = options } else { options := exportoptions.NewNonAppStoreOptions(method) options.CompileBitcode = (configs.CompileBitcode == "yes") options.TeamID = configs.TeamID exportOpts = options } log.Detail("generated export options content:") fmt.Println() fmt.Println(exportOpts.String()) if err = exportOpts.WriteToFile(exportOptionsPath); err != nil { fail("Failed to write export options to file, error: %s", err) } } fmt.Println() tmpDir, err := pathutil.NormalizedOSTempDirPath("__export__") if err != nil { fail("Failed to create tmp dir, error: %s", err) } exportCmd := xcodebuild.NewExportCommand() exportCmd.SetArchivePath(tmpArchivePath) exportCmd.SetExportDir(tmpDir) exportCmd.SetExportOptionsPlist(exportOptionsPath) if configs.OutputTool == "xcpretty" { xcprettyCmd := xcpretty.New(exportCmd) logWithTimestamp(colorstring.Green, xcprettyCmd.PrintableCmd()) fmt.Println() if xcodebuildOut, err := xcprettyCmd.Run(); err != nil { // xcodebuild raw output if err := utils.ExportOutputFileContent(xcodebuildOut, rawXcodebuildOutputLogPath, bitriseXcodeRawResultTextEnvKey); err != nil { log.Warn("Failed to export %s, error: %s", bitriseXcodeRawResultTextEnvKey, err) } else { log.Warn(`If you can't find the reason of the error in the log, please check the raw-xcodebuild-output.log The log file is stored in $BITRISE_DEPLOY_DIR, and its full path is available in the $BITRISE_XCODE_RAW_RESULT_TEXT_PATH environment variable`) } // xcdistributionlogs if logsDirPth, err := findIDEDistrubutionLogsPath(xcodebuildOut); err != nil { log.Warn("Failed to find xcdistributionlogs, error: %s", err) } else if err := utils.ExportOutputDirAsZip(logsDirPth, ideDistributionLogsZipPath, bitriseIDEDistributionLogsPthEnvKey); err != nil { log.Warn("Failed to export %s, error: %s", bitriseIDEDistributionLogsPthEnvKey, err) } else { log.Warn(`Also please check the xcdistributionlogs The logs directory is stored in $BITRISE_DEPLOY_DIR, and its full path is available in the $BITRISE_IDEDISTRIBUTION_LOGS_PATH environment variable`) } fail("Export failed, error: %s", err) } } else { logWithTimestamp(colorstring.Green, exportCmd.PrintableCmd()) fmt.Println() if xcodebuildOut, err := exportCmd.RunAndReturnOutput(); err != nil { // xcdistributionlogs if logsDirPth, err := findIDEDistrubutionLogsPath(xcodebuildOut); err != nil { log.Warn("Failed to find xcdistributionlogs, error: %s", err) } else if err := utils.ExportOutputDirAsZip(logsDirPth, ideDistributionLogsZipPath, bitriseIDEDistributionLogsPthEnvKey); err != nil { log.Warn("Failed to export %s, error: %s", bitriseIDEDistributionLogsPthEnvKey, err) } else { log.Warn(`If you can't find the reason of the error in the log, please check the xcdistributionlogs The logs directory is stored in $BITRISE_DEPLOY_DIR, and its full path is available in the $BITRISE_IDEDISTRIBUTION_LOGS_PATH environment variable`) } fail("Export failed, error: %s", err) } } // Search for ipa pattern := filepath.Join(tmpDir, "*.ipa") ipas, err := filepath.Glob(pattern) if err != nil { fail("Failed to collect ipa files, error: %s", err) } if len(ipas) == 0 { fail("No ipa found with pattern: %s", pattern) } else if len(ipas) == 1 { if err := cmdex.CopyFile(ipas[0], ipaPath); err != nil { fail("Failed to copy (%s) -> (%s), error: %s", ipas[0], ipaPath, err) } } else { log.Warn("More than 1 .ipa file found") for _, ipa := range ipas { base := filepath.Base(ipa) deployPth := filepath.Join(configs.OutputDir, base) if err := cmdex.CopyFile(ipa, deployPth); err != nil { fail("Failed to copy (%s) -> (%s), error: %s", ipas[0], ipaPath, err) } ipaPath = ipa } } } log.Info("Exporting outputs...") // // Export outputs // Export .xcarchive fmt.Println() if err := utils.ExportOutputDir(tmpArchivePath, tmpArchivePath, bitriseXCArchivePthEnvKey); err != nil { fail("Failed to export %s, error: %s", bitriseXCArchivePthEnvKey, err) } log.Done("The xcarchive path is now available in the Environment Variable: %s (value: %s)", bitriseXCArchivePthEnvKey, tmpArchivePath) if configs.IsExportXcarchiveZip == "yes" { if err := utils.ExportOutputDirAsZip(tmpArchivePath, archiveZipPath, bitriseXCArchiveZipPthEnvKey); err != nil { fail("Failed to export %s, error: %s", bitriseXCArchiveZipPthEnvKey, err) } log.Done("The xcarchive zip path is now available in the Environment Variable: %s (value: %s)", bitriseXCArchiveZipPthEnvKey, archiveZipPath) } // Export .app fmt.Println() exportedApp, err := xcarchive.FindApp(tmpArchivePath) if err != nil { fail("Failed to find app, error: %s", err) } if err := utils.ExportOutputDir(exportedApp, exportedApp, bitriseAppDirPthEnvKey); err != nil { fail("Failed to export %s, error: %s", bitriseAppDirPthEnvKey, err) } log.Done("The app directory is now available in the Environment Variable: %s (value: %s)", bitriseAppDirPthEnvKey, appPath) // Export .ipa fmt.Println() if err := utils.ExportOutputFile(ipaPath, ipaPath, bitriseIPAPthEnvKey); err != nil { fail("Failed to export %s, error: %s", bitriseIPAPthEnvKey, err) } log.Done("The ipa path is now available in the Environment Variable: %s (value: %s)", bitriseIPAPthEnvKey, ipaPath) // Export .dSYMs fmt.Println() appDSYM, frameworkDSYMs, err := xcarchive.FindDSYMs(tmpArchivePath) if err != nil { fail("Failed to export dsyms, error: %s", err) } dsymDir, err := pathutil.NormalizedOSTempDirPath("__dsyms__") if err != nil { fail("Failed to create tmp dir, error: %s", err) } if err := cmdex.CopyDir(appDSYM, dsymDir, false); err != nil { fail("Failed to copy (%s) -> (%s), error: %s", appDSYM, dsymDir, err) } if configs.ExportAllDsyms == "yes" { for _, dsym := range frameworkDSYMs { if err := cmdex.CopyDir(dsym, dsymDir, false); err != nil { fail("Failed to copy (%s) -> (%s), error: %s", dsym, dsymDir, err) } } } if err := utils.ExportOutputDir(dsymDir, dsymDir, bitriseDSYMDirPthEnvKey); err != nil { fail("Failed to export %s, error: %s", bitriseDSYMDirPthEnvKey, err) } log.Done("The dSYM dir path is now available in the Environment Variable: %s (value: %s)", bitriseDSYMDirPthEnvKey, dsymDir) if err := utils.ExportOutputDirAsZip(dsymDir, dsymZipPath, bitriseDSYMPthEnvKey); err != nil { fail("Failed to export %s, error: %s", bitriseDSYMPthEnvKey, err) } log.Done("The dSYM zip path is now available in the Environment Variable: %s (value: %s)", bitriseDSYMPthEnvKey, dsymZipPath) }