diff --git a/Gopkg.toml b/Gopkg.toml index 806b1257..9e3048cf 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -1,26 +1,3 @@ - -# Gopkg.toml example -# -# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" - - [[constraint]] name = "github.com/bitrise-io/go-utils" branch = "master" diff --git a/bitriseclient/certificate.go b/bitriseio/bitrise/certificate.go similarity index 89% rename from bitriseclient/certificate.go rename to bitriseio/bitrise/certificate.go index c60cbfc6..855f9c4b 100644 --- a/bitriseclient/certificate.go +++ b/bitriseio/bitrise/certificate.go @@ -1,4 +1,4 @@ -package bitriseclient +package bitrise import ( "math/big" @@ -76,7 +76,7 @@ type IdentityResponse struct { } // FetchUploadedIdentities ... -func (client *BitriseClient) FetchUploadedIdentities() ([]IdentityListData, error) { +func (client *Client) FetchUploadedIdentities() ([]IdentityListData, error) { log.Debugf("\nDownloading provisioning profile list from Bitrise...") requestURL, err := urlutil.Join(baseURL, appsEndPoint, client.selectedAppSlug, certificatesEndPoint) @@ -104,7 +104,7 @@ func (client *BitriseClient) FetchUploadedIdentities() ([]IdentityListData, erro } // GetUploadedCertificatesSerialby ... -func (client *BitriseClient) GetUploadedCertificatesSerialby(identitySlug string) (certificateSerialList []big.Int, err error) { +func (client *Client) GetUploadedCertificatesSerialby(identitySlug string) (certificateSerialList []big.Int, err error) { downloadURL, certificatePassword, err := client.getUploadedIdentityDownloadURLBy(identitySlug) if err != nil { return nil, err @@ -128,7 +128,7 @@ func (client *BitriseClient) GetUploadedCertificatesSerialby(identitySlug string return serialList, nil } -func (client *BitriseClient) getUploadedIdentityDownloadURLBy(certificateSlug string) (downloadURL string, password string, err error) { +func (client *Client) getUploadedIdentityDownloadURLBy(certificateSlug string) (downloadURL string, password string, err error) { log.Debugf("\nGet downloadURL for certificate (slug - %s) from Bitrise...", certificateSlug) requestURL, err := urlutil.Join(baseURL, appsEndPoint, client.selectedAppSlug, certificatesEndPoint, certificateSlug) @@ -157,7 +157,7 @@ func (client *BitriseClient) getUploadedIdentityDownloadURLBy(certificateSlug st return requestResponse.Data.DownloadURL, requestResponse.Data.CertificatePassword, nil } -func (client *BitriseClient) downloadUploadedIdentity(downloadURL string) (content string, err error) { +func (client *Client) downloadUploadedIdentity(downloadURL string) (content string, err error) { log.Debugf("\nDownloading identities from Bitrise...") log.Debugf("\nRequest URL: %s", downloadURL) @@ -182,7 +182,7 @@ func (client *BitriseClient) downloadUploadedIdentity(downloadURL string) (conte } // RegisterIdentity ... -func (client *BitriseClient) RegisterIdentity(certificateSize int64) (RegisterIdentityData, error) { +func (client *Client) RegisterIdentity(certificateSize int64) (RegisterIdentityData, error) { log.Printf("Register %s on Bitrise...", "Identities.p12") requestURL, err := urlutil.Join(baseURL, appsEndPoint, client.selectedAppSlug, certificatesEndPoint) @@ -217,7 +217,7 @@ func (client *BitriseClient) RegisterIdentity(certificateSize int64) (RegisterId } // UploadIdentity ... -func (client *BitriseClient) UploadIdentity(uploadURL string, uploadFileName string, outputDirPath string, exportFileName string) error { +func (client *Client) UploadIdentity(uploadURL string, uploadFileName string, outputDirPath string, exportFileName string) error { log.Printf("Upload %s to Bitrise...", exportFileName) filePth := filepath.Join(outputDirPath, exportFileName) @@ -235,7 +235,7 @@ func (client *BitriseClient) UploadIdentity(uploadURL string, uploadFileName str } // ConfirmIdentityUpload ... -func (client *BitriseClient) ConfirmIdentityUpload(certificateSlug string, certificateUploadName string) error { +func (client *Client) ConfirmIdentityUpload(certificateSlug string, certificateUploadName string) error { log.Printf("Confirm - %s - upload to Bitrise...", certificateUploadName) requestURL, err := urlutil.Join(baseURL, appsEndPoint, client.selectedAppSlug, "build-certificates", certificateSlug, "uploaded") diff --git a/bitriseclient/bitriseclient.go b/bitriseio/bitrise/client.go similarity index 90% rename from bitriseclient/bitriseclient.go rename to bitriseio/bitrise/client.go index a6ad13fa..060ad07b 100644 --- a/bitriseclient/bitriseclient.go +++ b/bitriseio/bitrise/client.go @@ -1,4 +1,4 @@ -package bitriseclient +package bitrise import ( "bytes" @@ -58,17 +58,17 @@ type MyAppsResponse struct { Paging Paging `json:"paging"` } -// BitriseClient ... -type BitriseClient struct { +// Client ... +type Client struct { accessToken string selectedAppSlug string headers map[string]string client http.Client } -// NewBitriseClient ... -func NewBitriseClient(accessToken string) (*BitriseClient, []Application, error) { - client := &BitriseClient{accessToken, "", map[string]string{"Authorization": "token " + accessToken}, http.Client{}} +// NewClient ... +func NewClient(accessToken string) (*Client, []Application, error) { + client := &Client{accessToken, "", map[string]string{"Authorization": "token " + accessToken}, http.Client{}} var apps []Application log.Infof("Fetching your application list from Bitrise...") @@ -120,12 +120,12 @@ func NewBitriseClient(accessToken string) (*BitriseClient, []Application, error) } // SetSelectedAppSlug ... -func (client *BitriseClient) SetSelectedAppSlug(slug string) { +func (client *Client) SetSelectedAppSlug(slug string) { client.selectedAppSlug = slug } // RunRequest ... -func RunRequest(client *BitriseClient, req *http.Request, requestResponse interface{}) (interface{}, []byte, error) { +func RunRequest(client *Client, req *http.Request, requestResponse interface{}) (interface{}, []byte, error) { var responseBody []byte if err := retry.Times(1).Wait(5 * time.Second).Try(func(attempt uint) error { @@ -201,7 +201,7 @@ func createRequest(requestMethod string, url string, headers map[string]string, return req, nil } -func performRequest(bitriseClient *BitriseClient, request *http.Request) (body []byte, statusCode int, err error) { +func performRequest(bitriseClient *Client, request *http.Request) (body []byte, statusCode int, err error) { response, err := bitriseClient.client.Do(request) if err != nil { // On error, any Response can be ignored diff --git a/bitriseclient/profile.go b/bitriseio/bitrise/profile.go similarity index 87% rename from bitriseclient/profile.go rename to bitriseio/bitrise/profile.go index 870b0f40..ac3d8f1f 100644 --- a/bitriseclient/profile.go +++ b/bitriseio/bitrise/profile.go @@ -1,4 +1,4 @@ -package bitriseclient +package bitrise import ( "net/http" @@ -72,7 +72,7 @@ type UploadedProvisioningProfileResponse struct { } // FetchProvisioningProfiles ... -func (client *BitriseClient) FetchProvisioningProfiles() ([]ProvisioningProfileListData, error) { +func (client *Client) FetchProvisioningProfiles() ([]ProvisioningProfileListData, error) { log.Debugf("\nDownloading provisioning profile list from Bitrise...") requestURL, err := urlutil.Join(baseURL, appsEndPoint, client.selectedAppSlug, provisioningProfilesEndPoint) @@ -102,7 +102,7 @@ func (client *BitriseClient) FetchProvisioningProfiles() ([]ProvisioningProfileL } // GetUploadedProvisioningProfileUUIDby ... -func (client *BitriseClient) GetUploadedProvisioningProfileUUIDby(profileSlug string) (UUID string, err error) { +func (client *Client) GetUploadedProvisioningProfileUUIDby(profileSlug string) (UUID string, err error) { downloadURL, err := client.getUploadedProvisioningProfileDownloadURLBy(profileSlug) if err != nil { return "", err @@ -126,7 +126,7 @@ func (client *BitriseClient) GetUploadedProvisioningProfileUUIDby(profileSlug st return data.UUID, nil } -func (client *BitriseClient) getUploadedProvisioningProfileDownloadURLBy(profileSlug string) (downloadURL string, err error) { +func (client *Client) getUploadedProvisioningProfileDownloadURLBy(profileSlug string) (downloadURL string, err error) { log.Debugf("\nGet downloadURL for provisioning profile (slug - %s) from Bitrise...", profileSlug) requestURL, err := urlutil.Join(baseURL, appsEndPoint, client.selectedAppSlug, provisioningProfilesEndPoint, profileSlug) @@ -155,7 +155,7 @@ func (client *BitriseClient) getUploadedProvisioningProfileDownloadURLBy(profile return requestResponse.Data.DownloadURL, nil } -func (client *BitriseClient) downloadUploadedProvisioningProfile(downloadURL string) (content string, err error) { +func (client *Client) downloadUploadedProvisioningProfile(downloadURL string) (content string, err error) { log.Debugf("\nDownloading provisioning profile from Bitrise...") log.Debugf("\nRequest URL: %s", downloadURL) @@ -180,7 +180,7 @@ func (client *BitriseClient) downloadUploadedProvisioningProfile(downloadURL str } // RegisterProvisioningProfile ... -func (client *BitriseClient) RegisterProvisioningProfile(provisioningProfSize int64, profile profileutil.ProvisioningProfileInfoModel) (RegisterProvisioningProfileData, error) { +func (client *Client) RegisterProvisioningProfile(provisioningProfSize int64, profile profileutil.ProvisioningProfileInfoModel) (RegisterProvisioningProfileData, error) { log.Printf("Register %s on Bitrise...", profile.Name) requestURL, err := urlutil.Join(baseURL, appsEndPoint, client.selectedAppSlug, provisioningProfilesEndPoint) @@ -215,7 +215,7 @@ func (client *BitriseClient) RegisterProvisioningProfile(provisioningProfSize in } // UploadProvisioningProfile ... -func (client *BitriseClient) UploadProvisioningProfile(uploadURL string, uploadFileName string, outputDirPath string, exportFileName string) error { +func (client *Client) UploadProvisioningProfile(uploadURL string, uploadFileName string, outputDirPath string, exportFileName string) error { log.Printf("Upload %s to Bitrise...", exportFileName) filePth := filepath.Join(outputDirPath, exportFileName) @@ -236,7 +236,7 @@ func (client *BitriseClient) UploadProvisioningProfile(uploadURL string, uploadF } // ConfirmProvisioningProfileUpload ... -func (client *BitriseClient) ConfirmProvisioningProfileUpload(profileSlug string, provUploadName string) error { +func (client *Client) ConfirmProvisioningProfileUpload(profileSlug string, provUploadName string) error { log.Printf("Confirm - %s - upload to Bitrise...", provUploadName) requestURL, err := urlutil.Join(baseURL, appsEndPoint, client.selectedAppSlug, provisioningProfilesEndPoint, profileSlug, "uploaded") diff --git a/bitriseio/bitriseio.go b/bitriseio/bitriseio.go new file mode 100644 index 00000000..c40f22dc --- /dev/null +++ b/bitriseio/bitriseio.go @@ -0,0 +1,293 @@ +package bitriseio + +import ( + "errors" + "fmt" + "os" + "regexp" + "strings" + + "github.com/bitrise-io/go-utils/colorstring" + "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-io/go-utils/sliceutil" + "github.com/bitrise-io/goinp/goinp" + "github.com/bitrise-tools/codesigndoc/bitriseio/bitrise" + "github.com/bitrise-tools/go-xcode/certificateutil" + "github.com/bitrise-tools/go-xcode/profileutil" +) + +// UploadCodesigningFiles ... +func UploadCodesigningFiles(certificates []certificateutil.CertificateInfoModel, profiles []profileutil.ProvisioningProfileInfoModel, outputDirPath string) (bool, bool, error) { + accessToken, err := askAccessToken() + if err != nil { + return false, false, err + } + + bitriseClient, appList, err := bitrise.NewClient(accessToken) + if err != nil { + return false, false, err + } + + selectedAppSlug, err := selectApp(appList) + if err != nil { + return false, false, err + } + + bitriseClient.SetSelectedAppSlug(selectedAppSlug) + + provProfilesUploaded, err := uploadExportedProvProfiles(bitriseClient, profiles, outputDirPath) + if err != nil { + return false, false, err + } + + certsUploaded, err := uploadExportedIdentity(bitriseClient, certificates, outputDirPath) + if err != nil { + return false, false, err + } + return certsUploaded, provProfilesUploaded, nil +} + +func askAccessToken() (token string, err error) { + messageToAsk := `Please copy your personal access token to Bitrise. +(To acquire a Personal Access Token for your user, sign in with that user on bitrise.io, go to your Account Settings page, +and select the Security tab on the left side.)` + fmt.Println() + + accesToken, err := goinp.AskForStringFromReader(messageToAsk, os.Stdin) + if err != nil { + return accesToken, err + } + + fmt.Println() + log.Infof("%s %s", colorstring.Green("Given accesToken:"), accesToken) + fmt.Println() + + return accesToken, nil +} + +func selectApp(appList []bitrise.Application) (seledtedAppSlug string, err error) { + var selectionList []string + + for _, app := range appList { + selectionList = append(selectionList, app.Title+" ("+app.RepoURL+")") + } + userSelection, err := goinp.SelectFromStringsWithDefault("Select the app which you want to upload the privisioning profiles", 1, selectionList) + + if err != nil { + return "", fmt.Errorf("failed to read input: %s", err) + + } + + log.Debugf("selected app: %v", userSelection) + + for index, selected := range selectionList { + if selected == userSelection { + return appList[index].Slug, nil + } + } + + return "", errors.New("failed to find selected app in appList") +} + +func uploadExportedProvProfiles(bitriseClient *bitrise.Client, profilesToExport []profileutil.ProvisioningProfileInfoModel, outputDirPath string) (bool, error) { + fmt.Println() + log.Infof("Uploading provisioning profiles...") + + profilesToUpload, err := filterAlreadyUploadedProvProfiles(bitriseClient, profilesToExport) + if err != nil { + return false, err + } + + if len(profilesToUpload) > 0 { + if err := uploadProvisioningProfiles(bitriseClient, profilesToUpload, outputDirPath); err != nil { + return false, err + } + } else { + log.Warnf("There is no new provisioning profile to upload...") + } + + return true, nil +} + +func filterAlreadyUploadedProvProfiles(client *bitrise.Client, localProfiles []profileutil.ProvisioningProfileInfoModel) ([]profileutil.ProvisioningProfileInfoModel, error) { + log.Printf("Looking for provisioning profile duplicates on Bitrise...") + + uploadedProfileUUIDList := map[string]bool{} + profilesToUpload := []profileutil.ProvisioningProfileInfoModel{} + + uploadedProfInfoList, err := client.FetchProvisioningProfiles() + if err != nil { + return nil, err + } + + for _, uploadedProfileInfo := range uploadedProfInfoList { + uploadedProfileUUID, err := client.GetUploadedProvisioningProfileUUIDby(uploadedProfileInfo.Slug) + if err != nil { + return nil, err + } + + uploadedProfileUUIDList[uploadedProfileUUID] = true + } + + for _, localProfile := range localProfiles { + contains, _ := uploadedProfileUUIDList[localProfile.UUID] + if contains { + log.Warnf("Already on Bitrise: - %s - (UUID: %s) ", localProfile.Name, localProfile.UUID) + } else { + profilesToUpload = append(profilesToUpload, localProfile) + } + } + + return profilesToUpload, nil +} + +func uploadProvisioningProfiles(bitriseClient *bitrise.Client, profilesToUpload []profileutil.ProvisioningProfileInfoModel, outputDirPath string) error { + for _, profile := range profilesToUpload { + exportFileName := provProfileExportFileName(profile, outputDirPath) + + provProfile, err := os.Open(outputDirPath + "/" + exportFileName) + if err != nil { + return err + } + + defer func() { + if err := provProfile.Close(); err != nil { + log.Warnf("Provisioning profile close failed, err: %s", err) + } + + }() + + info, err := provProfile.Stat() + if err != nil { + return err + } + + log.Debugf("\n%s size: %d", exportFileName, info.Size()) + + provProfSlugResponseData, err := bitriseClient.RegisterProvisioningProfile(info.Size(), profile) + if err != nil { + return err + } + + if err := bitriseClient.UploadProvisioningProfile(provProfSlugResponseData.UploadURL, provProfSlugResponseData.UploadFileName, outputDirPath, exportFileName); err != nil { + return err + } + + if err := bitriseClient.ConfirmProvisioningProfileUpload(provProfSlugResponseData.Slug, provProfSlugResponseData.UploadFileName); err != nil { + return err + } + } + + return nil +} + +func provProfileExportFileName(info profileutil.ProvisioningProfileInfoModel, path string) string { + replaceRexp, err := regexp.Compile("[^A-Za-z0-9_.-]") + if err != nil { + log.Warnf("Invalid regex, error: %s", err) + return "" + } + safeTitle := replaceRexp.ReplaceAllString(info.Name, "") + extension := ".mobileprovision" + if strings.HasSuffix(path, ".provisionprofile") { + extension = ".provisionprofile" + } + + return info.UUID + "." + safeTitle + extension +} + +func uploadExportedIdentity(bitriseClient *bitrise.Client, certificatesToExport []certificateutil.CertificateInfoModel, outputDirPath string) (bool, error) { + fmt.Println() + log.Infof("Uploading certificate...") + + shouldUploadIdentities, err := shouldUploadCertificates(bitriseClient, certificatesToExport) + if err != nil { + return false, err + } + + if shouldUploadIdentities { + + if err := uploadIdentity(bitriseClient, outputDirPath); err != nil { + return false, err + } + } else { + log.Warnf("There is no new certificate to upload...") + } + + return true, err +} + +func shouldUploadCertificates(client *bitrise.Client, certificatesToExport []certificateutil.CertificateInfoModel) (bool, error) { + log.Printf("Looking for certificate duplicates on Bitrise...") + + var uploadedCertificatesSerialList []string + localCertificatesSerialList := []string{} + + uploadedItentityList, err := client.FetchUploadedIdentities() + if err != nil { + return false, err + } + + // Get uploaded certificates' serials + for _, uploadedIdentity := range uploadedItentityList { + var serialListAsString []string + + serialList, err := client.GetUploadedCertificatesSerialby(uploadedIdentity.Slug) + if err != nil { + return false, err + } + + for _, serial := range serialList { + serialListAsString = append(serialListAsString, serial.String()) + } + uploadedCertificatesSerialList = append(uploadedCertificatesSerialList, serialListAsString...) + } + + for _, certificateToExport := range certificatesToExport { + localCertificatesSerialList = append(localCertificatesSerialList, certificateToExport.Serial) + } + + log.Debugf("Uploaded certificates' serial list: \n\t%v", uploadedCertificatesSerialList) + log.Debugf("Local certificates' serial list: \n\t%v", localCertificatesSerialList) + + // Search for a new certificate + for _, localCertificateSerial := range localCertificatesSerialList { + if !sliceutil.IsStringInSlice(localCertificateSerial, uploadedCertificatesSerialList) { + return true, nil + } + } + + return false, nil +} + +func uploadIdentity(bitriseClient *bitrise.Client, outputDirPath string) error { + identities, err := os.Open(outputDirPath + "/" + "Identities.p12") + if err != nil { + return err + } + + defer func() { + if err := identities.Close(); err != nil { + log.Warnf("Identities failed, err: %s", err) + } + + }() + + info, err := identities.Stat() + if err != nil { + return err + } + + log.Debugf("\n%s size: %d", "Identities.p12", info.Size()) + + certificateResponseData, err := bitriseClient.RegisterIdentity(info.Size()) + if err != nil { + return err + } + + if err := bitriseClient.UploadIdentity(certificateResponseData.UploadURL, certificateResponseData.UploadFileName, outputDirPath, "Identities.p12"); err != nil { + return err + } + + return bitriseClient.ConfirmIdentityUpload(certificateResponseData.Slug, certificateResponseData.UploadFileName) +} diff --git a/cmd/common.go b/cmd/common.go deleted file mode 100644 index 3c62b4e0..00000000 --- a/cmd/common.go +++ /dev/null @@ -1,918 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - "sort" - "strings" - - "github.com/bitrise-io/go-utils/colorstring" - "github.com/bitrise-io/go-utils/command" - "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-utils/pathutil" - "github.com/bitrise-io/go-utils/sliceutil" - "github.com/bitrise-io/goinp/goinp" - "github.com/bitrise-tools/codesigndoc/bitriseclient" - "github.com/bitrise-tools/codesigndoc/osxkeychain" - "github.com/bitrise-tools/go-xcode/certificateutil" - "github.com/bitrise-tools/go-xcode/export" - "github.com/bitrise-tools/go-xcode/exportoptions" - "github.com/bitrise-tools/go-xcode/profileutil" - "github.com/bitrise-tools/go-xcode/xcarchive" - "github.com/pkg/errors" -) - -const collectCodesigningFilesInfo = `To collect available code sign files, we search for installed Provisioning Profiles:" -- which has installed Codesign Identity in your Keychain" -- which can provision your application target's bundle ids" -- which has the project defined Capabilities set" -- which matches to the selected ipa export method" -` - -func initExportOutputDir() (string, error) { - confExportOutputDirPath := "./codesigndoc_exports" - absExportOutputDirPath, err := pathutil.AbsPath(confExportOutputDirPath) - log.Debugf("absExportOutputDirPath: %s", absExportOutputDirPath) - if err != nil { - return absExportOutputDirPath, fmt.Errorf("Failed to determin Absolute path of export dir: %s", confExportOutputDirPath) - } - if exist, err := pathutil.IsDirExists(absExportOutputDirPath); err != nil { - return absExportOutputDirPath, fmt.Errorf("Failed to determin whether the export directory already exists: %s", err) - } else if !exist { - if err := os.Mkdir(absExportOutputDirPath, 0777); err != nil { - return absExportOutputDirPath, fmt.Errorf("Failed to create export output directory at path: %s | error: %s", absExportOutputDirPath, err) - } - } else { - log.Warnf("Export output dir already exists at path: %s", absExportOutputDirPath) - } - return absExportOutputDirPath, nil -} - -func analyzeArchive(archive xcarchive.IosArchive, installedCertificates []certificateutil.CertificateInfoModel) (export.IosCodeSignGroup, error) { - signingIdentity := archive.SigningIdentity() - bundleIDProfileInfoMap := archive.BundleIDProfileInfoMap() - - if signingIdentity == "" { - return export.IosCodeSignGroup{}, fmt.Errorf("no signing identity found") - } - - certificate, err := findCertificate(signingIdentity, installedCertificates) - if err != nil { - return export.IosCodeSignGroup{}, err - } - - return export.IosCodeSignGroup{ - Certificate: certificate, - BundleIDProfileMap: bundleIDProfileInfoMap, - }, nil -} - -func collectIpaExportSelectableCodeSignGroups(archive xcarchive.IosArchive, installedCertificates []certificateutil.CertificateInfoModel, installedProfiles []profileutil.ProvisioningProfileInfoModel) []export.SelectableCodeSignGroup { - bundleIDEntitlemenstMap := archive.BundleIDEntitlementsMap() - - fmt.Println() - fmt.Println() - log.Infof("Targets to sign:") - fmt.Println() - for bundleID, entitlements := range bundleIDEntitlemenstMap { - fmt.Printf("- %s with %d capabilities\n", bundleID, len(entitlements)) - } - fmt.Println() - - bundleIDs := []string{} - for bundleID := range bundleIDEntitlemenstMap { - bundleIDs = append(bundleIDs, bundleID) - } - codeSignGroups := export.CreateSelectableCodeSignGroups(installedCertificates, installedProfiles, bundleIDs) - - log.Debugf("Codesign Groups:") - for _, group := range codeSignGroups { - log.Debugf(group.String()) - } - - if len(codeSignGroups) == 0 { - return []export.SelectableCodeSignGroup{} - } - - codeSignGroups = export.FilterSelectableCodeSignGroups(codeSignGroups, - export.CreateEntitlementsSelectableCodeSignGroupFilter(bundleIDEntitlemenstMap), - ) - - // Handle if archive used NON xcode managed profile - if len(codeSignGroups) > 0 && !archive.IsXcodeManaged() { - log.Warnf("App was signed with NON xcode managed profile when archiving,") - log.Warnf("only NOT xcode managed profiles are allowed to sign when exporting the archive.") - log.Warnf("Removing xcode managed CodeSignInfo groups") - - codeSignGroups = export.FilterSelectableCodeSignGroups(codeSignGroups, - export.CreateNotXcodeManagedSelectableCodeSignGroupFilter(), - ) - } - - log.Debugf("\n") - log.Debugf("Filtered Codesign Groups:") - for _, group := range codeSignGroups { - log.Debugf(group.String()) - } - - return codeSignGroups -} - -func filterLatestProfiles(profiles []profileutil.ProvisioningProfileInfoModel) []profileutil.ProvisioningProfileInfoModel { - profilesByBundleIDAndName := map[string][]profileutil.ProvisioningProfileInfoModel{} - for _, profile := range profiles { - bundleID := profile.BundleID - name := profile.Name - bundleIDAndName := bundleID + name - profs, ok := profilesByBundleIDAndName[bundleIDAndName] - if !ok { - profs = []profileutil.ProvisioningProfileInfoModel{} - } - profs = append(profs, profile) - profilesByBundleIDAndName[bundleIDAndName] = profs - } - - filteredProfiles := []profileutil.ProvisioningProfileInfoModel{} - for _, profiles := range profilesByBundleIDAndName { - var latestProfile *profileutil.ProvisioningProfileInfoModel - for _, profile := range profiles { - if latestProfile == nil || profile.ExpirationDate.After(latestProfile.ExpirationDate) { - latestProfile = &profile - } - } - filteredProfiles = append(filteredProfiles, *latestProfile) - } - return filteredProfiles -} - -func collectIpaExportCodeSignGroups(tool Tool, archive xcarchive.IosArchive, installedCertificates []certificateutil.CertificateInfoModel, installedProfiles []profileutil.ProvisioningProfileInfoModel) ([]export.IosCodeSignGroup, error) { - iosCodeSignGroups := []export.IosCodeSignGroup{} - - codeSignGroups := collectIpaExportSelectableCodeSignGroups(archive, installedCertificates, installedProfiles) - if len(codeSignGroups) == 0 { - return nil, errors.New("no code sign files (Codesign Identities and Provisioning Profiles) are installed to export an ipa\n" + collectCodesigningFilesInfo) - } - - exportMethods := []string{"development", "app-store", "ad-hoc", "enterprise"} - - for true { - fmt.Println() - selectedExportMethod, err := goinp.SelectFromStringsWithDefault("Select the ipa export method", 1, exportMethods) - if err != nil { - return nil, fmt.Errorf("failed to read input: %s", err) - } - log.Debugf("selected export method: %v", selectedExportMethod) - - fmt.Println() - filteredCodeSignGroups := export.FilterSelectableCodeSignGroups(codeSignGroups, - export.CreateExportMethodSelectableCodeSignGroupFilter(exportoptions.Method(selectedExportMethod)), - ) - - log.Debugf("\n") - log.Debugf("Filtered Codesign Groups:") - for _, group := range codeSignGroups { - log.Debugf(group.String()) - } - - if len(filteredCodeSignGroups) == 0 { - fmt.Println() - log.Errorf(collectCodesigningFilesInfo) - fmt.Println() - fmt.Println() - question := "Do you want to collect another ipa export code sign files" - question += "\n(select NO to finish collecting codesign files and continue)" - anotherExport, err := goinp.AskForBoolWithDefault(question, false) - if err != nil { - return nil, fmt.Errorf("failed to read input: %s", err) - } - if !anotherExport { - break - } - continue - } - - // Select certificate - certificates := []certificateutil.CertificateInfoModel{} - certificateOptions := []string{} - for _, group := range filteredCodeSignGroups { - certificate := group.Certificate - certificates = append(certificates, certificate) - certificateOption := fmt.Sprintf("%s [%s] - development team: %s", certificate.CommonName, certificate.Serial, certificate.TeamName) - certificateOptions = append(certificateOptions, certificateOption) - } - - selectedCertificateOption := "" - if len(certificateOptions) == 1 { - selectedCertificateOption = certificateOptions[0] - - fmt.Printf("Codesign Indentity for %s ipa export: %s\n", selectedExportMethod, selectedCertificateOption) - } else { - sort.Strings(certificateOptions) - - fmt.Println() - question := fmt.Sprintf("Select the Codesign Indentity for %s ipa export", selectedExportMethod) - selectedCertificateOption, err = goinp.SelectFromStringsWithDefault(question, 1, certificateOptions) - if err != nil { - return nil, fmt.Errorf("failed to read input: %s", err) - } - } - - var selectedCertificate *certificateutil.CertificateInfoModel - for _, certificate := range certificates { - option := fmt.Sprintf("%s [%s] - development team: %s", certificate.CommonName, certificate.Serial, certificate.TeamName) - if option == selectedCertificateOption { - selectedCertificate = &certificate - break - } - } - if selectedCertificate == nil { - return nil, errors.New("failed to find selected Codesign Indentity") - } - - // Select Profiles - bundleIDProfilesMap := map[string][]profileutil.ProvisioningProfileInfoModel{} - for _, group := range filteredCodeSignGroups { - option := fmt.Sprintf("%s [%s] - development team: %s", group.Certificate.CommonName, group.Certificate.Serial, group.Certificate.TeamName) - if option == selectedCertificateOption { - bundleIDProfilesMap = group.BundleIDProfilesMap - break - } - } - if len(bundleIDProfilesMap) == 0 { - return nil, errors.New("failed to find Provisioning Profiles for Code Sign Identity") - } - - selectedBundleIDProfileMap := map[string]profileutil.ProvisioningProfileInfoModel{} - for bundleID, profiles := range bundleIDProfilesMap { - profiles = filterLatestProfiles(profiles) - profileOptions := []string{} - for _, profile := range profiles { - profileOption := fmt.Sprintf("%s (%s)", profile.Name, profile.UUID) - profileOptions = append(profileOptions, profileOption) - } - - selectedProfileOption := "" - if len(profileOptions) == 1 { - selectedProfileOption = profileOptions[0] - - fmt.Printf("Provisioning Profile to sign target (%s): %s\n", bundleID, selectedProfileOption) - } else { - sort.Strings(profileOptions) - - fmt.Println() - question := fmt.Sprintf("Select the Provisioning Profile to sign target with bundle ID: %s", bundleID) - selectedProfileOption, err = goinp.SelectFromStringsWithDefault(question, 1, profileOptions) - if err != nil { - return nil, fmt.Errorf("failed to read input: %s", err) - } - } - - for _, profile := range profiles { - option := fmt.Sprintf("%s (%s)", profile.Name, profile.UUID) - if option == selectedProfileOption { - selectedBundleIDProfileMap[bundleID] = profile - } - } - } - if len(selectedBundleIDProfileMap) != len(bundleIDProfilesMap) { - return nil, fmt.Errorf("failed to find Provisioning Profiles for ipa export") - } - - iosCodeSignGroup := export.IosCodeSignGroup{ - Certificate: *selectedCertificate, - BundleIDProfileMap: selectedBundleIDProfileMap, - } - - fmt.Println() - fmt.Println() - log.Infof("Codesign settings will be used for %s ipa export:", exportMethod(iosCodeSignGroup)) - fmt.Println() - printCodesignGroup(iosCodeSignGroup) - - iosCodeSignGroups = append(iosCodeSignGroups, iosCodeSignGroup) - - fmt.Println() - fmt.Println() - question := "Do you want to collect another ipa export code sign files" - question += "\n(select NO to finish collecting codesign files and continue)" - anotherExport, err := goinp.AskForBoolWithDefault(question, false) - if err != nil { - return nil, fmt.Errorf("failed to read input: %s", err) - } - if !anotherExport { - break - } - } - - return iosCodeSignGroups, nil -} - -func collectIpaExportCertificate(tool Tool, archiveCertificate certificateutil.CertificateInfoModel, installedCertificates []certificateutil.CertificateInfoModel) (certificateutil.CertificateInfoModel, error) { - fmt.Println() - fmt.Println() - question := fmt.Sprintf(`The archive used codesigning files of team: %s - %s -Would you like to use this team to sign your project?`, archiveCertificate.TeamID, archiveCertificate.TeamName) - useArchiveTeam, err := goinp.AskForBoolWithDefault(question, true) - if err != nil { - return certificateutil.CertificateInfoModel{}, fmt.Errorf("failed to read input: %s", err) - } - - selectedTeam := "" - certificatesByTeam := mapCertificatesByTeam(installedCertificates) - - if !useArchiveTeam { - teams := []string{} - for team := range certificatesByTeam { - teams = append(teams, team) - } - - fmt.Println() - selectedTeam, err = goinp.SelectFromStringsWithDefault("Select the Development team to sign your project", 1, teams) - if err != nil { - return certificateutil.CertificateInfoModel{}, fmt.Errorf("failed to read input: %s", err) - } - } else { - selectedTeam = fmt.Sprintf("%s - %s", archiveCertificate.TeamID, archiveCertificate.TeamName) - } - - selectedCertificate := certificateutil.CertificateInfoModel{} - - if isDistributionCertificate(archiveCertificate) { - certificates := certificatesByTeam[selectedTeam] - developmentCertificates := certificateutil.FilterCertificateInfoModelsByFilterFunc(certificates, func(certInfo certificateutil.CertificateInfoModel) bool { - return !isDistributionCertificate(certInfo) - }) - - certificateOptions := []string{} - for _, certInfo := range developmentCertificates { - certificateOption := fmt.Sprintf("%s [%s]", certInfo.CommonName, certInfo.Serial) - certificateOptions = append(certificateOptions, certificateOption) - } - - fmt.Println() - question := fmt.Sprintf(`The Xcode archive used distribution certificate: %s [%s]. -Please select a development certificate:`, archiveCertificate.CommonName, archiveCertificate.Serial) - selectedCertificateOption, err := goinp.SelectFromStringsWithDefault(question, 1, certificateOptions) - if err != nil { - return certificateutil.CertificateInfoModel{}, fmt.Errorf("failed to read input: %s", err) - } - - for _, certInfo := range developmentCertificates { - certificateOption := fmt.Sprintf("%s [%s]", certInfo.CommonName, certInfo.Serial) - if certificateOption == selectedCertificateOption { - selectedCertificate = certInfo - } - } - } else { - certificates := certificatesByTeam[selectedTeam] - distributionCertificates := certificateutil.FilterCertificateInfoModelsByFilterFunc(certificates, func(certInfo certificateutil.CertificateInfoModel) bool { - return isDistributionCertificate(certInfo) - }) - - certificateOptions := []string{} - for _, certInfo := range distributionCertificates { - certificateOption := fmt.Sprintf("%s [%s]", certInfo.CommonName, certInfo.Serial) - certificateOptions = append(certificateOptions, certificateOption) - } - - fmt.Println() - question := fmt.Sprintf(`The Xcode archive used development certificate: %s [%s]. -Please select a distribution certificate:`, archiveCertificate.CommonName, archiveCertificate.Serial) - selectedCertificateOption, err := goinp.SelectFromStringsWithDefault(question, 1, certificateOptions) - if err != nil { - return certificateutil.CertificateInfoModel{}, fmt.Errorf("failed to read input: %s", err) - } - - for _, certInfo := range distributionCertificates { - certificateOption := fmt.Sprintf("%s [%s]", certInfo.CommonName, certInfo.Serial) - if certificateOption == selectedCertificateOption { - selectedCertificate = certInfo - } - } - } - - return selectedCertificate, nil -} - -func collectAndExportProvisioningProfiles(profiles []profileutil.ProvisioningProfileInfoModel, absExportOutputDirPath string) error { - if len(profiles) == 0 { - return nil - } - - fmt.Println() - log.Infof("Required Provisioning Profiles (%d)", len(profiles)) - fmt.Println() - for _, profile := range profiles { - log.Printf("- %s (UUID: %s)", profile.Name, profile.UUID) - } - - profilePathInfoMap := map[string]profileutil.ProvisioningProfileInfoModel{} - - fmt.Println() - log.Infof("Exporting Provisioning Profiles...") - fmt.Println() - - for _, profile := range profiles { - log.Printf("searching for required Provisioning Profile: %s (UUID: %s)", profile.Name, profile.UUID) - _, pth, err := profileutil.FindProvisioningProfileInfo(profile.UUID) - if err != nil { - return errors.Wrap(err, "failed to find Provisioning Profile") - } - profilePathInfoMap[pth] = profile - log.Printf("file found at: %s", pth) - } - - if err := exportProvisioningProfiles(profilePathInfoMap, absExportOutputDirPath); err != nil { - return fmt.Errorf("failed to export the Provisioning Profile into the export directory: %s", err) - } - - return nil -} - -func collectAndExportIdentities(certificates []certificateutil.CertificateInfoModel, absExportOutputDirPath string) error { - if len(certificates) == 0 { - return nil - } - - fmt.Println() - fmt.Println() - log.Infof("Required Identities/Certificates (%d)", len(certificates)) - fmt.Println() - for _, certificate := range certificates { - log.Printf("- %s", certificate.CommonName) - } - - fmt.Println() - log.Infof("Exporting the Identities (Certificates):") - fmt.Println() - - identitiesWithKeychainRefs := []osxkeychain.IdentityWithRefModel{} - defer osxkeychain.ReleaseIdentityWithRefList(identitiesWithKeychainRefs) - - for _, certificate := range certificates { - log.Printf("searching for Identity: %s", certificate.CommonName) - identityRef, err := osxkeychain.FindAndValidateIdentity(certificate.CommonName) - if err != nil { - return fmt.Errorf("failed to export, error: %s", err) - } - - if identityRef == nil { - return errors.New("identity not found in the keychain, or it was invalid (expired)") - } - - identitiesWithKeychainRefs = append(identitiesWithKeychainRefs, *identityRef) - } - - identityKechainRefs := osxkeychain.CreateEmptyCFTypeRefSlice() - for _, aIdentityWithRefItm := range identitiesWithKeychainRefs { - fmt.Println("exporting Identity:", aIdentityWithRefItm.Label) - identityKechainRefs = append(identityKechainRefs, aIdentityWithRefItm.KeychainRef) - } - - fmt.Println() - if isAskForPassword { - log.Infof("Exporting from Keychain") - log.Warnf(" You'll be asked to provide a Passphrase for the .p12 file!") - } else { - log.Warnf("Exporting from Keychain using empty Passphrase...") - log.Printf("This means that if you want to import the file the passphrase at import should be left empty,") - log.Printf("you don't have to type in anything, just leave the passphrase input empty.") - } - fmt.Println() - log.Warnf("You'll most likely see popups one for each Identity from Keychain,") - log.Warnf("you will have to accept (Allow) those to be able to export the Identities!") - fmt.Println() - - if err := osxkeychain.ExportFromKeychain(identityKechainRefs, filepath.Join(absExportOutputDirPath, "Identities.p12"), isAskForPassword); err != nil { - return fmt.Errorf("failed to export from Keychain: %s", err) - } - - return nil -} - -func exportProvisioningProfiles(profilePathInfoMap map[string]profileutil.ProvisioningProfileInfoModel, exportTargetDirPath string) error { - idx := -1 - for path, profileInfo := range profilePathInfoMap { - idx++ - - if idx != 0 { - fmt.Println() - } - - log.Printf("exporting Provisioning Profile: %s (%s)", profileInfo.Name, profileInfo.UUID) - - exportFileName := provProfileExportFileName(profileInfo, path) - exportPth := filepath.Join(exportTargetDirPath, exportFileName) - if err := command.RunCommand("cp", path, exportPth); err != nil { - return fmt.Errorf("Failed to copy Provisioning Profile (from: %s) (to: %s), error: %s", path, exportPth, err) - } - } - return nil -} - -func provProfileExportFileName(info profileutil.ProvisioningProfileInfoModel, path string) string { - replaceRexp, err := regexp.Compile("[^A-Za-z0-9_.-]") - if err != nil { - log.Warnf("Invalid regex, error: %s", err) - return "" - } - safeTitle := replaceRexp.ReplaceAllString(info.Name, "") - extension := ".mobileprovision" - if strings.HasSuffix(path, ".provisionprofile") { - extension = ".provisionprofile" - } - - return info.UUID + "." + safeTitle + extension -} - -func exportCodesignFiles(tool Tool, archivePath, outputDirPath string) error { - // archive code sign settings - installedCertificates, err := certificateutil.InstalledCodesigningCertificateInfos() - if err != nil { - return fmt.Errorf("failed to list installed code signing identities, error: %s", err) - } - installedCertificates = certificateutil.FilterValidCertificateInfos(installedCertificates) - - log.Debugf("Installed certificates:") - for _, installedCertificate := range installedCertificates { - log.Debugf(installedCertificate.String()) - } - - installedProfiles, err := profileutil.InstalledProvisioningProfileInfos(profileutil.ProfileTypeIos) - if err != nil { - return fmt.Errorf("failed to list installed provisioning profiles, error: %s", err) - } - - log.Debugf("Installed profiles:") - for _, profileInfo := range installedProfiles { - log.Debugf(profileInfo.String(installedCertificates...)) - } - - archive, err := xcarchive.NewIosArchive(archivePath) - if err != nil { - return fmt.Errorf("failed to analyze archive, error: %s", err) - } - - archiveCodeSignGroup, err := analyzeArchive(archive, installedCertificates) - if err != nil { - return fmt.Errorf("failed to analyze the archive, error: %s", err) - } - - fmt.Println() - log.Infof("Codesign settings used for archive:") - fmt.Println() - printCodesignGroup(archiveCodeSignGroup) - - // ipa export code sign settings - fmt.Println() - fmt.Println() - log.Printf("🔦 Analyzing the archive, to get ipa export code signing settings...") - - certificatesToExport := []certificateutil.CertificateInfoModel{} - profilesToExport := []profileutil.ProvisioningProfileInfoModel{} - - if certificatesOnly { - ipaExportCertificate, err := collectIpaExportCertificate(tool, archiveCodeSignGroup.Certificate, installedCertificates) - if err != nil { - return err - } - - certificatesToExport = append(certificatesToExport, archiveCodeSignGroup.Certificate, ipaExportCertificate) - } else { - ipaExportCodeSignGroups, err := collectIpaExportCodeSignGroups(tool, archive, installedCertificates, installedProfiles) - if err != nil { - return err - } - - if len(ipaExportCodeSignGroups) == 0 { - return errors.New("no ipa export code sign groups collected") - } - - codeSignGroups := append(ipaExportCodeSignGroups, archiveCodeSignGroup) - certificates, profiles := extractCertificatesAndProfiles(codeSignGroups...) - certificatesToExport = append(certificatesToExport, certificates...) - profilesToExport = append(profilesToExport, profiles...) - } - - if err := collectAndExportIdentities(certificatesToExport, outputDirPath); err != nil { - return err - } - - if err := collectAndExportProvisioningProfiles(profilesToExport, outputDirPath); err != nil { - return err - } - - provProfilesUploaded := (len(profilesToExport) == 0) - certsUploaded := (len(certificatesToExport) == 0) - - if len(profilesToExport) > 0 || len(certificatesToExport) > 0 { - fmt.Println() - shouldUpload, err := askUploadGeneratedFiles() - if err != nil { - return err - } - - if shouldUpload { - accessToken, err := getAccessToken() - if err != nil { - return err - } - - bitriseClient, appList, err := bitriseclient.NewBitriseClient(accessToken) - if err != nil { - return err - } - - selectedAppSlug, err := selectApp(appList) - if err != nil { - return err - } - - bitriseClient.SetSelectedAppSlug(selectedAppSlug) - - provProfilesUploaded, err = uploadExportedProvProfiles(bitriseClient, profilesToExport, outputDirPath) - if err != nil { - return err - } - - certsUploaded, err = uploadExportedIdentity(bitriseClient, certificatesToExport, outputDirPath) - if err != nil { - return err - } - } - } - - fmt.Println() - log.Successf("Exports finished you can find the exported files at: %s", outputDirPath) - - if err := command.RunCommand("open", outputDirPath); err != nil { - log.Errorf("Failed to open the export directory in Finder: %s", outputDirPath) - } else { - fmt.Println("Opened the directory in Finder.") - } - printFinished(provProfilesUploaded, certsUploaded) - - return nil -} - -func getAccessToken() (string, error) { - accessToken, err := askAccessToken() - if err != nil { - return "", err - } - - return accessToken, nil -} - -func uploadExportedProvProfiles(bitriseClient *bitriseclient.BitriseClient, profilesToExport []profileutil.ProvisioningProfileInfoModel, outputDirPath string) (bool, error) { - fmt.Println() - log.Infof("Uploading provisioning profiles...") - - profilesToUpload, err := filterAlreadyUploadedProvProfiles(bitriseClient, profilesToExport) - if err != nil { - return false, err - } - - if len(profilesToUpload) > 0 { - if err := uploadProvisioningProfiles(bitriseClient, profilesToUpload, outputDirPath); err != nil { - return false, err - } - } else { - log.Warnf("There is no new provisioning profile to upload...") - } - - return true, nil -} - -func uploadExportedIdentity(bitriseClient *bitriseclient.BitriseClient, certificatesToExport []certificateutil.CertificateInfoModel, outputDirPath string) (bool, error) { - fmt.Println() - log.Infof("Uploading certificate...") - - shouldUploadIdentities, err := shouldUploadCertificates(bitriseClient, certificatesToExport) - if err != nil { - return false, err - } - - if shouldUploadIdentities { - - if err := UploadIdentity(bitriseClient, outputDirPath); err != nil { - return false, err - } - } else { - log.Warnf("There is no new certificate to upload...") - } - - return true, err -} - -func askUploadGeneratedFiles() (bool, error) { - messageToAsk := "Do you want to upload the provisioning profiles and certificates to Bitrise?" - return goinp.AskForBoolFromReader(messageToAsk, os.Stdin) -} - -func askUploadIdentities() (bool, error) { - messageToAsk := "Do you want to upload the certificates to Bitrise?" - return goinp.AskForBoolFromReader(messageToAsk, os.Stdin) -} - -func filterAlreadyUploadedProvProfiles(client *bitriseclient.BitriseClient, localProfiles []profileutil.ProvisioningProfileInfoModel) ([]profileutil.ProvisioningProfileInfoModel, error) { - log.Printf("Looking for provisioning profile duplicates on Bitrise...") - - uploadedProfileUUIDList := map[string]bool{} - profilesToUpload := []profileutil.ProvisioningProfileInfoModel{} - - uploadedProfInfoList, err := client.FetchProvisioningProfiles() - if err != nil { - return nil, err - } - - for _, uploadedProfileInfo := range uploadedProfInfoList { - uploadedProfileUUID, err := client.GetUploadedProvisioningProfileUUIDby(uploadedProfileInfo.Slug) - if err != nil { - return nil, err - } - - uploadedProfileUUIDList[uploadedProfileUUID] = true - } - - for _, localProfile := range localProfiles { - contains, _ := uploadedProfileUUIDList[localProfile.UUID] - if contains { - log.Warnf("Already on Bitrise: - %s - (UUID: %s) ", localProfile.Name, localProfile.UUID) - } else { - profilesToUpload = append(profilesToUpload, localProfile) - } - } - - return profilesToUpload, nil -} - -func shouldUploadCertificates(client *bitriseclient.BitriseClient, certificatesToExport []certificateutil.CertificateInfoModel) (bool, error) { - log.Printf("Looking for certificate duplicates on Bitrise...") - - var uploadedCertificatesSerialList []string - localCertificatesSerialList := []string{} - - uploadedItentityList, err := client.FetchUploadedIdentities() - if err != nil { - return false, err - } - - // Get uploaded certificates' serials - for _, uploadedIdentity := range uploadedItentityList { - var serialListAsString []string - - serialList, err := client.GetUploadedCertificatesSerialby(uploadedIdentity.Slug) - if err != nil { - return false, err - } - - for _, serial := range serialList { - serialListAsString = append(serialListAsString, serial.String()) - } - uploadedCertificatesSerialList = append(uploadedCertificatesSerialList, serialListAsString...) - } - - for _, certificateToExport := range certificatesToExport { - localCertificatesSerialList = append(localCertificatesSerialList, certificateToExport.Serial) - } - - log.Debugf("Uploaded certificates' serial list: \n\t%v", uploadedCertificatesSerialList) - log.Debugf("Local certificates' serial list: \n\t%v", localCertificatesSerialList) - - // Search for a new certificate - for _, localCertificateSerial := range localCertificatesSerialList { - if !sliceutil.IsStringInSlice(localCertificateSerial, uploadedCertificatesSerialList) { - return true, nil - } - } - - return false, nil -} - -// ---------------------------------------------------------------- -// --- Upload methods -func uploadProvisioningProfiles(bitriseClient *bitriseclient.BitriseClient, profilesToUpload []profileutil.ProvisioningProfileInfoModel, outputDirPath string) error { - for _, profile := range profilesToUpload { - exportFileName := provProfileExportFileName(profile, outputDirPath) - - provProfile, err := os.Open(outputDirPath + "/" + exportFileName) - if err != nil { - return err - } - - defer func() { - if err := provProfile.Close(); err != nil { - log.Warnf("Provisioning profile close failed, err: %s", err) - } - - }() - - info, err := provProfile.Stat() - if err != nil { - return err - } - - log.Debugf("\n%s size: %d", exportFileName, info.Size()) - - provProfSlugResponseData, err := bitriseClient.RegisterProvisioningProfile(info.Size(), profile) - if err != nil { - return err - } - - if err := bitriseClient.UploadProvisioningProfile(provProfSlugResponseData.UploadURL, provProfSlugResponseData.UploadFileName, outputDirPath, exportFileName); err != nil { - return err - } - - if err := bitriseClient.ConfirmProvisioningProfileUpload(provProfSlugResponseData.Slug, provProfSlugResponseData.UploadFileName); err != nil { - return err - } - } - - return nil -} - -// UploadIdentity ... -func UploadIdentity(bitriseClient *bitriseclient.BitriseClient, outputDirPath string) error { - identities, err := os.Open(outputDirPath + "/" + "Identities.p12") - if err != nil { - return err - } - - defer func() { - if err := identities.Close(); err != nil { - log.Warnf("Identities failed, err: %s", err) - } - - }() - - info, err := identities.Stat() - if err != nil { - return err - } - - log.Debugf("\n%s size: %d", "Identities.p12", info.Size()) - - certificateResponseData, err := bitriseClient.RegisterIdentity(info.Size()) - if err != nil { - return err - } - - if err := bitriseClient.UploadIdentity(certificateResponseData.UploadURL, certificateResponseData.UploadFileName, outputDirPath, "Identities.p12"); err != nil { - return err - } - - return bitriseClient.ConfirmIdentityUpload(certificateResponseData.Slug, certificateResponseData.UploadFileName) -} - -func askAccessToken() (token string, err error) { - messageToAsk := `Please copy your personal access token to Bitrise. -(To acquire a Personal Access Token for your user, sign in with that user on bitrise.io, go to your Account Settings page, -and select the Security tab on the left side.)` - fmt.Println() - - accesToken, err := goinp.AskForStringFromReader(messageToAsk, os.Stdin) - if err != nil { - return accesToken, err - } - - fmt.Println() - log.Infof("%s %s", colorstring.Green("Given accesToken:"), accesToken) - fmt.Println() - - return accesToken, nil -} - -func selectApp(appList []bitriseclient.Application) (seledtedAppSlug string, err error) { - var selectionList []string - - for _, app := range appList { - selectionList = append(selectionList, app.Title+" ("+app.RepoURL+")") - } - userSelection, err := goinp.SelectFromStringsWithDefault("Select the app which you want to upload the privisioning profiles", 1, selectionList) - - if err != nil { - return "", fmt.Errorf("failed to read input: %s", err) - - } - - log.Debugf("selected app: %v", userSelection) - - for index, selected := range selectionList { - if selected == userSelection { - return appList[index].Slug, nil - } - } - - return "", &appSelectionError{"failed to find selected app in appList"} -} - -type appSelectionError struct { - s string -} - -func (e *appSelectionError) Error() string { - return e.s -} diff --git a/cmd/errors.go b/cmd/errors.go deleted file mode 100644 index c8e0aab6..00000000 --- a/cmd/errors.go +++ /dev/null @@ -1,31 +0,0 @@ -package cmd - -import "github.com/bitrise-io/go-utils/colorstring" - -// Tool ... -type Tool string - -const ( - toolXcode Tool = "Xcode" - toolXamarin Tool = "Visual Studio" -) - -// ArchiveError ... -type ArchiveError struct { - tool Tool - msg string -} - -// Error ... -func (e ArchiveError) Error() string { - return ` -------------------------------` + ` -First of all ` + colorstring.Red("please make sure that you can Archive your app from "+e.tool+".") + ` -codesigndoc only works if you can archive your app from ` + string(e.tool) + `. -If you can, and you get a valid IPA file if you export from ` + string(e.tool) + `, -` + colorstring.Red("please create an issue") + ` on GitHub at: https://github.com/bitrise-tools/codesigndoc/issues -with as many details & logs as you can share! ------------------------------- - -` + colorstring.Redf("Error: %s", e.msg) -} diff --git a/cmd/print.go b/cmd/print.go deleted file mode 100644 index 1c9fb7c0..00000000 --- a/cmd/print.go +++ /dev/null @@ -1,34 +0,0 @@ -package cmd - -import ( - "fmt" - "strings" - - "github.com/bitrise-io/go-utils/colorstring" - "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-tools/go-xcode/export" -) - -func printFinished(provProfilesUploaded bool, certsUploaded bool) { - fmt.Println() - log.Successf("That's all.") - - if !provProfilesUploaded && !certsUploaded { - log.Warnf("You just have to upload the found certificates (.p12) and provisioning profiles (.mobileprovision) and you'll be good to go!") - fmt.Println() - } -} - -func printCodesignGroup(group export.IosCodeSignGroup) { - fmt.Printf("%s %s (%s)\n", colorstring.Green("development team:"), group.Certificate.TeamName, group.Certificate.TeamID) - fmt.Printf("%s %s [%s]\n", colorstring.Green("codesign identity:"), group.Certificate.CommonName, group.Certificate.Serial) - idx := -1 - for bundleID, profile := range group.BundleIDProfileMap { - idx++ - if idx == 0 { - fmt.Printf("%s %s -> %s\n", colorstring.Greenf("provisioning profiles:"), profile.Name, bundleID) - } else { - fmt.Printf("%s%s -> %s\n", strings.Repeat(" ", len("provisioning profiles: ")), profile.Name, bundleID) - } - } -} diff --git a/cmd/scan.go b/cmd/scan.go index b4abad8b..b40067ef 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -1,6 +1,12 @@ package cmd -import "github.com/spf13/cobra" +import ( + "fmt" + + "github.com/bitrise-io/go-utils/colorstring" + "github.com/bitrise-io/go-utils/log" + "github.com/spf13/cobra" +) // scanCmd represents the scan command var scanCmd = &cobra.Command{ @@ -23,3 +29,41 @@ func init() { scanCmd.PersistentFlags().BoolVar(&isAskForPassword, "ask-pass", false, "Ask for .p12 password, instead of using an empty password") scanCmd.PersistentFlags().BoolVar(&certificatesOnly, "certs-only", false, "Collect Certificates (Identities) only") } + +// Tool ... +type Tool string + +const ( + toolXcode Tool = "Xcode" + toolXamarin Tool = "Visual Studio" +) + +// ArchiveError ... +type ArchiveError struct { + tool Tool + msg string +} + +// Error ... +func (e ArchiveError) Error() string { + return ` +------------------------------` + ` +First of all ` + colorstring.Red("please make sure that you can Archive your app from "+e.tool+".") + ` +codesigndoc only works if you can archive your app from ` + string(e.tool) + `. +If you can, and you get a valid IPA file if you export from ` + string(e.tool) + `, +` + colorstring.Red("please create an issue") + ` on GitHub at: https://github.com/bitrise-tools/codesigndoc/issues +with as many details & logs as you can share! +------------------------------ + +` + colorstring.Redf("Error: %s", e.msg) +} + +func printFinished(provProfilesUploaded bool, certsUploaded bool) { + fmt.Println() + log.Successf("That's all.") + + if !provProfilesUploaded && !certsUploaded { + log.Warnf("You just have to upload the found certificates (.p12) and provisioning profiles (.mobileprovision) and you'll be good to go!") + fmt.Println() + } +} diff --git a/cmd/utils.go b/cmd/utils.go deleted file mode 100644 index ed26b9e4..00000000 --- a/cmd/utils.go +++ /dev/null @@ -1,72 +0,0 @@ -package cmd - -import ( - "fmt" - "strings" - - "github.com/bitrise-tools/go-xcode/certificateutil" - "github.com/bitrise-tools/go-xcode/export" - "github.com/bitrise-tools/go-xcode/profileutil" - "github.com/pkg/errors" -) - -func extractCertificatesAndProfiles(codeSignGroups ...export.IosCodeSignGroup) ([]certificateutil.CertificateInfoModel, []profileutil.ProvisioningProfileInfoModel) { - certificateMap := map[string]certificateutil.CertificateInfoModel{} - profilesMap := map[string]profileutil.ProvisioningProfileInfoModel{} - for _, group := range codeSignGroups { - certificate := group.Certificate - - certificateMap[certificate.Serial] = certificate - - for _, profile := range group.BundleIDProfileMap { - profilesMap[profile.UUID] = profile - } - } - - certificates := []certificateutil.CertificateInfoModel{} - profiles := []profileutil.ProvisioningProfileInfoModel{} - for _, certificate := range certificateMap { - certificates = append(certificates, certificate) - } - for _, profile := range profilesMap { - profiles = append(profiles, profile) - } - return certificates, profiles -} - -func exportMethod(group export.IosCodeSignGroup) string { - for _, profile := range group.BundleIDProfileMap { - return string(profile.ExportType) - } - return "" -} - -func findCertificate(nameOrSHA1Fingerprint string, certificates []certificateutil.CertificateInfoModel) (certificateutil.CertificateInfoModel, error) { - for _, certificate := range certificates { - if certificate.CommonName == nameOrSHA1Fingerprint { - return certificate, nil - } - if strings.ToLower(certificate.SHA1Fingerprint) == strings.ToLower(nameOrSHA1Fingerprint) { - return certificate, nil - } - } - return certificateutil.CertificateInfoModel{}, errors.Errorf("installed certificate not found with common name or sha1 hash: %s", nameOrSHA1Fingerprint) -} - -func isDistributionCertificate(certificate certificateutil.CertificateInfoModel) bool { - return strings.HasPrefix(certificate.CommonName, "iPhone Distribution:") -} - -func mapCertificatesByTeam(certificates []certificateutil.CertificateInfoModel) map[string][]certificateutil.CertificateInfoModel { - certificatesByTeam := map[string][]certificateutil.CertificateInfoModel{} - for _, certificateInfo := range certificates { - team := fmt.Sprintf("%s - %s", certificateInfo.TeamID, certificateInfo.TeamName) - certs, ok := certificatesByTeam[team] - if !ok { - certs = []certificateutil.CertificateInfoModel{} - } - certs = append(certs, certificateInfo) - certificatesByTeam[team] = certs - } - return certificatesByTeam -} diff --git a/cmd/xamarin.go b/cmd/xamarin.go index d314338c..57fba764 100644 --- a/cmd/xamarin.go +++ b/cmd/xamarin.go @@ -10,6 +10,7 @@ import ( "github.com/bitrise-io/go-utils/fileutil" "github.com/bitrise-io/go-utils/log" "github.com/bitrise-io/goinp/goinp" + "github.com/bitrise-tools/codesigndoc/codesigndoc" "github.com/bitrise-tools/codesigndoc/xamarin" "github.com/bitrise-tools/go-xamarin/analyzers/project" "github.com/bitrise-tools/go-xamarin/analyzers/solution" @@ -175,5 +176,11 @@ and then hit Enter` return ArchiveError{toolXamarin, "failed to run xamarin build command: " + err.Error()} } - return exportCodesignFiles("Xamarin Studio", archivePath, absExportOutputDirPath) + certsUploaded, provProfilesUploaded, err := codesigndoc.ExportCodesignFiles(archivePath, absExportOutputDirPath, certificatesOnly, isAskForPassword) + if err != nil { + return err + } + + printFinished(provProfilesUploaded, certsUploaded) + return nil } diff --git a/cmd/xcode.go b/cmd/xcode.go index 7f45a2f4..b29a8cb7 100644 --- a/cmd/xcode.go +++ b/cmd/xcode.go @@ -2,13 +2,16 @@ package cmd import ( "fmt" + "os" "path/filepath" "strings" "github.com/bitrise-io/go-utils/colorstring" "github.com/bitrise-io/go-utils/fileutil" "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-io/go-utils/pathutil" "github.com/bitrise-io/goinp/goinp" + "github.com/bitrise-tools/codesigndoc/codesigndoc" "github.com/bitrise-tools/codesigndoc/xcode" "github.com/bitrise-tools/go-xcode/utility" "github.com/spf13/cobra" @@ -39,6 +42,25 @@ func init() { 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") } +func initExportOutputDir() (string, error) { + confExportOutputDirPath := "./codesigndoc_exports" + absExportOutputDirPath, err := pathutil.AbsPath(confExportOutputDirPath) + log.Debugf("absExportOutputDirPath: %s", absExportOutputDirPath) + if err != nil { + return absExportOutputDirPath, fmt.Errorf("Failed to determin Absolute path of export dir: %s", confExportOutputDirPath) + } + if exist, err := pathutil.IsDirExists(absExportOutputDirPath); err != nil { + return absExportOutputDirPath, fmt.Errorf("Failed to determin whether the export directory already exists: %s", err) + } else if !exist { + if err := os.Mkdir(absExportOutputDirPath, 0777); err != nil { + return absExportOutputDirPath, fmt.Errorf("Failed to create export output directory at path: %s | error: %s", absExportOutputDirPath, err) + } + } else { + log.Warnf("Export output dir already exists at path: %s", absExportOutputDirPath) + } + return absExportOutputDirPath, nil +} + func scanXcodeProject(cmd *cobra.Command, args []string) error { absExportOutputDirPath, err := initExportOutputDir() if err != nil { @@ -129,5 +151,11 @@ the one you usually open in Xcode, then hit Enter. return ArchiveError{toolXcode, err.Error()} } - return exportCodesignFiles("Xcode", archivePath, absExportOutputDirPath) + certsUploaded, provProfilesUploaded, err := codesigndoc.ExportCodesignFiles(archivePath, absExportOutputDirPath, certificatesOnly, isAskForPassword) + if err != nil { + return err + } + + printFinished(provProfilesUploaded, certsUploaded) + return nil } diff --git a/codesigndoc/archive.go b/codesigndoc/archive.go new file mode 100644 index 00000000..794fff49 --- /dev/null +++ b/codesigndoc/archive.go @@ -0,0 +1,29 @@ +package codesigndoc + +import ( + "fmt" + + "github.com/bitrise-tools/go-xcode/certificateutil" + "github.com/bitrise-tools/go-xcode/export" + "github.com/bitrise-tools/go-xcode/xcarchive" +) + +// analyzeArchive opens the generated archive and returns a codesign group, which holds the archive signing options +func analyzeArchive(archive xcarchive.IosArchive, installedCertificates []certificateutil.CertificateInfoModel) (export.IosCodeSignGroup, error) { + signingIdentity := archive.SigningIdentity() + bundleIDProfileInfoMap := archive.BundleIDProfileInfoMap() + + if signingIdentity == "" { + return export.IosCodeSignGroup{}, fmt.Errorf("no signing identity found") + } + + certificate, err := findCertificate(signingIdentity, installedCertificates) + if err != nil { + return export.IosCodeSignGroup{}, err + } + + return export.IosCodeSignGroup{ + Certificate: certificate, + BundleIDProfileMap: bundleIDProfileInfoMap, + }, nil +} diff --git a/codesigndoc/certificates.go b/codesigndoc/certificates.go new file mode 100644 index 00000000..35dccbca --- /dev/null +++ b/codesigndoc/certificates.go @@ -0,0 +1,110 @@ +package codesigndoc + +import ( + "fmt" + "strings" + + "github.com/bitrise-tools/go-xcode/certificateutil" + "github.com/pkg/errors" +) + +type certificateType uint8 + +// CertificateTypes ... +const ( + IOSCertificate certificateType = iota + MacOSCertificate + MacOSInstallerCertificate +) + +var ( + iOSCertificateNames = []string{ + "iPhone Developer", //type: "iOS Development" + "iPhone Distribution", // type: "iOS Distribution" + } + + macOSCertificateNames = []string{ + "Mac Developer", // type: "Mac Development" + "3rd Party Mac Developer Application", // type: "Mac App Distribution" + "Developer ID Application", // type: "Developer ID Application" + } + + macOSInstallerCertificateNames = []string{ + "3rd Party Mac Developer Installer", // type: "Mac Installer Distribution" + "Developer ID Installer", // type: "Developer ID Installer" + } +) + +// installedCertificates returns the certificate installed in the keychain, +// the expired certificates are removed from the list +func installedCertificates(certType certificateType) ([]certificateutil.CertificateInfoModel, error) { + var certs []certificateutil.CertificateInfoModel + var err error + + if certType == MacOSInstallerCertificate { + certs, err = certificateutil.InstalledInstallerCertificateInfos() + } else { + certs, err = certificateutil.InstalledCodesigningCertificateInfos() + if err == nil { + certs = certificateutil.FilterCertificateInfoModelsByFilterFunc(certs, func(cert certificateutil.CertificateInfoModel) bool { + var certNames []string + if certType == IOSCertificate { + certNames = iOSCertificateNames + } else { + certNames = macOSCertificateNames + } + + for _, name := range certNames { + if strings.Contains(strings.ToLower(cert.CommonName), strings.ToLower(name)) { + return true + } + } + return false + }) + } + } + + return certificateutil.FilterValidCertificateInfos(certs), nil +} + +// isDistributionCertificate returns true if the given certificate +// is an iOS Distribution, Mac App Distribution or Developer ID Application certificate +func isDistributionCertificate(cert certificateutil.CertificateInfoModel) bool { + if strings.Contains(strings.ToLower(cert.CommonName), strings.ToLower("iPhone Distribution")) { + return true + } + + if strings.Contains(strings.ToLower(cert.CommonName), strings.ToLower("3rd Party Mac Developer Application")) { + return true + } + + return false +} + +// mapCertificatesByTeam returns a certificate list mapped by the certificate's team (in teamdID - teamName format) +func mapCertificatesByTeam(certificates []certificateutil.CertificateInfoModel) map[string][]certificateutil.CertificateInfoModel { + certificatesByTeam := map[string][]certificateutil.CertificateInfoModel{} + for _, certificateInfo := range certificates { + team := fmt.Sprintf("%s - %s", certificateInfo.TeamID, certificateInfo.TeamName) + certs, ok := certificatesByTeam[team] + if !ok { + certs = []certificateutil.CertificateInfoModel{} + } + certs = append(certs, certificateInfo) + certificatesByTeam[team] = certs + } + return certificatesByTeam +} + +// findCertificate returns the first certificate, which's common_name or SHA1 fingerprint matches to the given string +func findCertificate(nameOrSHA1Fingerprint string, certificates []certificateutil.CertificateInfoModel) (certificateutil.CertificateInfoModel, error) { + for _, certificate := range certificates { + if certificate.CommonName == nameOrSHA1Fingerprint { + return certificate, nil + } + if strings.ToLower(certificate.SHA1Fingerprint) == strings.ToLower(nameOrSHA1Fingerprint) { + return certificate, nil + } + } + return certificateutil.CertificateInfoModel{}, errors.Errorf("installed certificate not found with common name or sha1 hash: %s", nameOrSHA1Fingerprint) +} diff --git a/codesigndoc/codesigndoc.go b/codesigndoc/codesigndoc.go new file mode 100644 index 00000000..a44e0e7e --- /dev/null +++ b/codesigndoc/codesigndoc.go @@ -0,0 +1,120 @@ +package codesigndoc + +import ( + "errors" + "fmt" + "os" + + "github.com/bitrise-io/go-utils/command" + "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-io/goinp/goinp" + "github.com/bitrise-tools/codesigndoc/bitriseio" + "github.com/bitrise-tools/go-xcode/certificateutil" + "github.com/bitrise-tools/go-xcode/profileutil" + "github.com/bitrise-tools/go-xcode/xcarchive" +) + +const collectCodesigningFilesInfo = `To collect available code sign files, we search for installed Provisioning Profiles:" +- which has installed Codesign Identity in your Keychain" +- which can provision your application target's bundle ids" +- which has the project defined Capabilities set" +- which matches to the selected ipa export method" +` + +// ExportCodesignFiles exports the codesigning files required to create an xcode archive +// and exports the codesigning files for the specified export method +func ExportCodesignFiles(archivePath, outputDirPath string, certificatesOnly bool, askForPassword bool) (bool, bool, error) { + certificates, err := installedCertificates(IOSCertificate) + if err != nil { + return false, false, fmt.Errorf("failed to list installed code signing identities, error: %s", err) + } + + profiles, err := profileutil.InstalledProvisioningProfileInfos(profileutil.ProfileTypeIos) + if err != nil { + return false, false, fmt.Errorf("failed to list installed provisioning profiles, error: %s", err) + } + + // archive code sign settings + archive, err := xcarchive.NewIosArchive(archivePath) + if err != nil { + return false, false, fmt.Errorf("failed to analyze archive, error: %s", err) + } + + archiveCodeSignGroup, err := analyzeArchive(archive, certificates) + if err != nil { + return false, false, fmt.Errorf("failed to analyze the archive, error: %s", err) + } + + fmt.Println() + log.Infof("Codesign settings used for archive:") + fmt.Println() + printCodesignGroup(archiveCodeSignGroup) + + // ipa export code sign settings + fmt.Println() + fmt.Println() + log.Printf("🔦 Analyzing the archive, to get ipa export code signing settings...") + + certificatesToExport := []certificateutil.CertificateInfoModel{} + profilesToExport := []profileutil.ProvisioningProfileInfoModel{} + + if certificatesOnly { + ipaExportCertificate, err := collectIpaExportCertificate(archiveCodeSignGroup.Certificate, certificates) + if err != nil { + return false, false, err + } + + certificatesToExport = append(certificatesToExport, archiveCodeSignGroup.Certificate, ipaExportCertificate) + } else { + ipaExportCodeSignGroups, err := collectIpaExportCodeSignGroups(archive, certificates, profiles) + if err != nil { + return false, false, err + } + + if len(ipaExportCodeSignGroups) == 0 { + return false, false, errors.New("no ipa export code sign groups collected") + } + + codeSignGroups := append(ipaExportCodeSignGroups, archiveCodeSignGroup) + certificates, profiles := extractCertificatesAndProfiles(codeSignGroups...) + certificatesToExport = append(certificatesToExport, certificates...) + profilesToExport = append(profilesToExport, profiles...) + } + + if err := collectAndExportIdentities(certificatesToExport, outputDirPath, askForPassword); err != nil { + return false, false, err + } + + if err := collectAndExportProvisioningProfiles(profilesToExport, outputDirPath); err != nil { + return false, false, err + } + + provProfilesUploaded := (len(profilesToExport) == 0) + certsUploaded := (len(certificatesToExport) == 0) + + if len(profilesToExport) > 0 || len(certificatesToExport) > 0 { + fmt.Println() + shouldUpload, err := goinp.AskForBoolFromReader("Do you want to upload the provisioning profiles and certificates to Bitrise?", os.Stdin) + if err != nil { + return false, false, err + } + + if shouldUpload { + certsUploaded, provProfilesUploaded, err = bitriseio.UploadCodesigningFiles(certificatesToExport, profilesToExport, outputDirPath) + if err != nil { + return false, false, err + } + } + } + + fmt.Println() + log.Successf("Exports finished you can find the exported files at: %s", outputDirPath) + + if err := command.RunCommand("open", outputDirPath); err != nil { + log.Errorf("Failed to open the export directory in Finder: %s", outputDirPath) + } else { + fmt.Println("Opened the directory in Finder.") + } + + return certsUploaded, provProfilesUploaded, nil +} diff --git a/codesigndoc/codesigngroup.go b/codesigndoc/codesigngroup.go new file mode 100644 index 00000000..c788e095 --- /dev/null +++ b/codesigndoc/codesigngroup.go @@ -0,0 +1,368 @@ +package codesigndoc + +import ( + "errors" + "fmt" + "sort" + "strings" + + "github.com/bitrise-io/go-utils/colorstring" + "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-io/goinp/goinp" + "github.com/bitrise-tools/go-xcode/certificateutil" + "github.com/bitrise-tools/go-xcode/export" + "github.com/bitrise-tools/go-xcode/exportoptions" + "github.com/bitrise-tools/go-xcode/profileutil" + "github.com/bitrise-tools/go-xcode/xcarchive" +) + +// extractCertificatesAndProfiles returns the certificates and provisioning profiles of the given codesign group +func extractCertificatesAndProfiles(codeSignGroups ...export.IosCodeSignGroup) ([]certificateutil.CertificateInfoModel, []profileutil.ProvisioningProfileInfoModel) { + certificateMap := map[string]certificateutil.CertificateInfoModel{} + profilesMap := map[string]profileutil.ProvisioningProfileInfoModel{} + for _, group := range codeSignGroups { + certificate := group.Certificate + + certificateMap[certificate.Serial] = certificate + + for _, profile := range group.BundleIDProfileMap { + profilesMap[profile.UUID] = profile + } + } + + certificates := []certificateutil.CertificateInfoModel{} + profiles := []profileutil.ProvisioningProfileInfoModel{} + for _, certificate := range certificateMap { + certificates = append(certificates, certificate) + } + for _, profile := range profilesMap { + profiles = append(profiles, profile) + } + return certificates, profiles +} + +// exportMethod returns which ipa/pkg/app export type is allowed by the given codesign group +func exportMethod(group export.IosCodeSignGroup) string { + for _, profile := range group.BundleIDProfileMap { + return string(profile.ExportType) + } + return "" +} + +// printCodesignGroup prints the given codesign group +func printCodesignGroup(group export.IosCodeSignGroup) { + fmt.Printf("%s %s (%s)\n", colorstring.Green("development team:"), group.Certificate.TeamName, group.Certificate.TeamID) + fmt.Printf("%s %s [%s]\n", colorstring.Green("codesign identity:"), group.Certificate.CommonName, group.Certificate.Serial) + idx := -1 + for bundleID, profile := range group.BundleIDProfileMap { + idx++ + if idx == 0 { + fmt.Printf("%s %s -> %s\n", colorstring.Greenf("provisioning profiles:"), profile.Name, bundleID) + } else { + fmt.Printf("%s%s -> %s\n", strings.Repeat(" ", len("provisioning profiles: ")), profile.Name, bundleID) + } + } +} + +// collectIpaExportCertificate returns the certificate to use for the ipa export +func collectIpaExportCertificate(archiveCertificate certificateutil.CertificateInfoModel, installedCertificates []certificateutil.CertificateInfoModel) (certificateutil.CertificateInfoModel, error) { + fmt.Println() + fmt.Println() + question := fmt.Sprintf(`The archive used codesigning files of team: %s - %s +Would you like to use this team to sign your project?`, archiveCertificate.TeamID, archiveCertificate.TeamName) + useArchiveTeam, err := goinp.AskForBoolWithDefault(question, true) + if err != nil { + return certificateutil.CertificateInfoModel{}, fmt.Errorf("failed to read input: %s", err) + } + + selectedTeam := "" + certificatesByTeam := mapCertificatesByTeam(installedCertificates) + + if !useArchiveTeam { + teams := []string{} + for team := range certificatesByTeam { + teams = append(teams, team) + } + + fmt.Println() + selectedTeam, err = goinp.SelectFromStringsWithDefault("Select the Development team to sign your project", 1, teams) + if err != nil { + return certificateutil.CertificateInfoModel{}, fmt.Errorf("failed to read input: %s", err) + } + } else { + selectedTeam = fmt.Sprintf("%s - %s", archiveCertificate.TeamID, archiveCertificate.TeamName) + } + + selectedCertificate := certificateutil.CertificateInfoModel{} + + if isDistributionCertificate(archiveCertificate) { + certificates := certificatesByTeam[selectedTeam] + developmentCertificates := certificateutil.FilterCertificateInfoModelsByFilterFunc(certificates, func(certInfo certificateutil.CertificateInfoModel) bool { + return !isDistributionCertificate(certInfo) + }) + + certificateOptions := []string{} + for _, certInfo := range developmentCertificates { + certificateOption := fmt.Sprintf("%s [%s]", certInfo.CommonName, certInfo.Serial) + certificateOptions = append(certificateOptions, certificateOption) + } + + fmt.Println() + question := fmt.Sprintf(`The Xcode archive used distribution certificate: %s [%s]. +Please select a development certificate:`, archiveCertificate.CommonName, archiveCertificate.Serial) + selectedCertificateOption, err := goinp.SelectFromStringsWithDefault(question, 1, certificateOptions) + if err != nil { + return certificateutil.CertificateInfoModel{}, fmt.Errorf("failed to read input: %s", err) + } + + for _, certInfo := range developmentCertificates { + certificateOption := fmt.Sprintf("%s [%s]", certInfo.CommonName, certInfo.Serial) + if certificateOption == selectedCertificateOption { + selectedCertificate = certInfo + } + } + } else { + certificates := certificatesByTeam[selectedTeam] + distributionCertificates := certificateutil.FilterCertificateInfoModelsByFilterFunc(certificates, func(certInfo certificateutil.CertificateInfoModel) bool { + return isDistributionCertificate(certInfo) + }) + + certificateOptions := []string{} + for _, certInfo := range distributionCertificates { + certificateOption := fmt.Sprintf("%s [%s]", certInfo.CommonName, certInfo.Serial) + certificateOptions = append(certificateOptions, certificateOption) + } + + fmt.Println() + question := fmt.Sprintf(`The Xcode archive used development certificate: %s [%s]. +Please select a distribution certificate:`, archiveCertificate.CommonName, archiveCertificate.Serial) + selectedCertificateOption, err := goinp.SelectFromStringsWithDefault(question, 1, certificateOptions) + if err != nil { + return certificateutil.CertificateInfoModel{}, fmt.Errorf("failed to read input: %s", err) + } + + for _, certInfo := range distributionCertificates { + certificateOption := fmt.Sprintf("%s [%s]", certInfo.CommonName, certInfo.Serial) + if certificateOption == selectedCertificateOption { + selectedCertificate = certInfo + } + } + } + + return selectedCertificate, nil +} + +// collectIpaExportCodeSignGroups returns the codesigngroups required to export an ipa with the selected export methods +func collectIpaExportCodeSignGroups(archive xcarchive.IosArchive, installedCertificates []certificateutil.CertificateInfoModel, installedProfiles []profileutil.ProvisioningProfileInfoModel) ([]export.IosCodeSignGroup, error) { + iosCodeSignGroups := []export.IosCodeSignGroup{} + + codeSignGroups := collectIpaExportSelectableCodeSignGroups(archive, installedCertificates, installedProfiles) + if len(codeSignGroups) == 0 { + return nil, errors.New("no code sign files (Codesign Identities and Provisioning Profiles) are installed to export an ipa\n" + collectCodesigningFilesInfo) + } + + exportMethods := []string{"development", "app-store", "ad-hoc", "enterprise"} + + for true { + fmt.Println() + selectedExportMethod, err := goinp.SelectFromStringsWithDefault("Select the ipa export method", 1, exportMethods) + if err != nil { + return nil, fmt.Errorf("failed to read input: %s", err) + } + log.Debugf("selected export method: %v", selectedExportMethod) + + fmt.Println() + filteredCodeSignGroups := export.FilterSelectableCodeSignGroups(codeSignGroups, + export.CreateExportMethodSelectableCodeSignGroupFilter(exportoptions.Method(selectedExportMethod)), + ) + + log.Debugf("\n") + log.Debugf("Filtered Codesign Groups:") + for _, group := range codeSignGroups { + log.Debugf(group.String()) + } + + if len(filteredCodeSignGroups) == 0 { + fmt.Println() + log.Errorf(collectCodesigningFilesInfo) + fmt.Println() + fmt.Println() + question := "Do you want to collect another ipa export code sign files" + question += "\n(select NO to finish collecting codesign files and continue)" + anotherExport, err := goinp.AskForBoolWithDefault(question, false) + if err != nil { + return nil, fmt.Errorf("failed to read input: %s", err) + } + if !anotherExport { + break + } + continue + } + + // Select certificate + certificates := []certificateutil.CertificateInfoModel{} + certificateOptions := []string{} + for _, group := range filteredCodeSignGroups { + certificate := group.Certificate + certificates = append(certificates, certificate) + certificateOption := fmt.Sprintf("%s [%s] - development team: %s", certificate.CommonName, certificate.Serial, certificate.TeamName) + certificateOptions = append(certificateOptions, certificateOption) + } + + selectedCertificateOption := "" + if len(certificateOptions) == 1 { + selectedCertificateOption = certificateOptions[0] + + fmt.Printf("Codesign Indentity for %s ipa export: %s\n", selectedExportMethod, selectedCertificateOption) + } else { + sort.Strings(certificateOptions) + + fmt.Println() + question := fmt.Sprintf("Select the Codesign Indentity for %s ipa export", selectedExportMethod) + selectedCertificateOption, err = goinp.SelectFromStringsWithDefault(question, 1, certificateOptions) + if err != nil { + return nil, fmt.Errorf("failed to read input: %s", err) + } + } + + var selectedCertificate *certificateutil.CertificateInfoModel + for _, certificate := range certificates { + option := fmt.Sprintf("%s [%s] - development team: %s", certificate.CommonName, certificate.Serial, certificate.TeamName) + if option == selectedCertificateOption { + selectedCertificate = &certificate + break + } + } + if selectedCertificate == nil { + return nil, errors.New("failed to find selected Codesign Indentity") + } + + // Select Profiles + bundleIDProfilesMap := map[string][]profileutil.ProvisioningProfileInfoModel{} + for _, group := range filteredCodeSignGroups { + option := fmt.Sprintf("%s [%s] - development team: %s", group.Certificate.CommonName, group.Certificate.Serial, group.Certificate.TeamName) + if option == selectedCertificateOption { + bundleIDProfilesMap = group.BundleIDProfilesMap + break + } + } + if len(bundleIDProfilesMap) == 0 { + return nil, errors.New("failed to find Provisioning Profiles for Code Sign Identity") + } + + selectedBundleIDProfileMap := map[string]profileutil.ProvisioningProfileInfoModel{} + for bundleID, profiles := range bundleIDProfilesMap { + profiles = filterLatestProfiles(profiles) + profileOptions := []string{} + for _, profile := range profiles { + profileOption := fmt.Sprintf("%s (%s)", profile.Name, profile.UUID) + profileOptions = append(profileOptions, profileOption) + } + + selectedProfileOption := "" + if len(profileOptions) == 1 { + selectedProfileOption = profileOptions[0] + + fmt.Printf("Provisioning Profile to sign target (%s): %s\n", bundleID, selectedProfileOption) + } else { + sort.Strings(profileOptions) + + fmt.Println() + question := fmt.Sprintf("Select the Provisioning Profile to sign target with bundle ID: %s", bundleID) + selectedProfileOption, err = goinp.SelectFromStringsWithDefault(question, 1, profileOptions) + if err != nil { + return nil, fmt.Errorf("failed to read input: %s", err) + } + } + + for _, profile := range profiles { + option := fmt.Sprintf("%s (%s)", profile.Name, profile.UUID) + if option == selectedProfileOption { + selectedBundleIDProfileMap[bundleID] = profile + } + } + } + if len(selectedBundleIDProfileMap) != len(bundleIDProfilesMap) { + return nil, fmt.Errorf("failed to find Provisioning Profiles for ipa export") + } + + iosCodeSignGroup := export.IosCodeSignGroup{ + Certificate: *selectedCertificate, + BundleIDProfileMap: selectedBundleIDProfileMap, + } + + fmt.Println() + fmt.Println() + log.Infof("Codesign settings will be used for %s ipa export:", exportMethod(iosCodeSignGroup)) + fmt.Println() + printCodesignGroup(iosCodeSignGroup) + + iosCodeSignGroups = append(iosCodeSignGroups, iosCodeSignGroup) + + fmt.Println() + fmt.Println() + question := "Do you want to collect another ipa export code sign files" + question += "\n(select NO to finish collecting codesign files and continue)" + anotherExport, err := goinp.AskForBoolWithDefault(question, false) + if err != nil { + return nil, fmt.Errorf("failed to read input: %s", err) + } + if !anotherExport { + break + } + } + + return iosCodeSignGroups, nil +} + +// collectIpaExportSelectableCodeSignGroups returns every possible codesigngroup which can be used to export an ipa file +func collectIpaExportSelectableCodeSignGroups(archive xcarchive.IosArchive, installedCertificates []certificateutil.CertificateInfoModel, installedProfiles []profileutil.ProvisioningProfileInfoModel) []export.SelectableCodeSignGroup { + bundleIDEntitlemenstMap := archive.BundleIDEntitlementsMap() + + fmt.Println() + fmt.Println() + log.Infof("Targets to sign:") + fmt.Println() + for bundleID, entitlements := range bundleIDEntitlemenstMap { + fmt.Printf("- %s with %d capabilities\n", bundleID, len(entitlements)) + } + fmt.Println() + + bundleIDs := []string{} + for bundleID := range bundleIDEntitlemenstMap { + bundleIDs = append(bundleIDs, bundleID) + } + codeSignGroups := export.CreateSelectableCodeSignGroups(installedCertificates, installedProfiles, bundleIDs) + + log.Debugf("Codesign Groups:") + for _, group := range codeSignGroups { + log.Debugf(group.String()) + } + + if len(codeSignGroups) == 0 { + return []export.SelectableCodeSignGroup{} + } + + codeSignGroups = export.FilterSelectableCodeSignGroups(codeSignGroups, + export.CreateEntitlementsSelectableCodeSignGroupFilter(bundleIDEntitlemenstMap), + ) + + // Handle if archive used NON xcode managed profile + if len(codeSignGroups) > 0 && !archive.IsXcodeManaged() { + log.Warnf("App was signed with NON xcode managed profile when archiving,") + log.Warnf("only NOT xcode managed profiles are allowed to sign when exporting the archive.") + log.Warnf("Removing xcode managed CodeSignInfo groups") + + codeSignGroups = export.FilterSelectableCodeSignGroups(codeSignGroups, + export.CreateNotXcodeManagedSelectableCodeSignGroupFilter(), + ) + } + + log.Debugf("\n") + log.Debugf("Filtered Codesign Groups:") + for _, group := range codeSignGroups { + log.Debugf(group.String()) + } + + return codeSignGroups +} diff --git a/codesigndoc/export.go b/codesigndoc/export.go new file mode 100644 index 00000000..aa198f42 --- /dev/null +++ b/codesigndoc/export.go @@ -0,0 +1,111 @@ +package codesigndoc + +import ( + "errors" + "fmt" + "path/filepath" + + "github.com/bitrise-io/go-utils/command" + "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-tools/codesigndoc/osxkeychain" + "github.com/bitrise-tools/go-xcode/certificateutil" + "github.com/bitrise-tools/go-xcode/profileutil" +) + +// collectAndExportIdentities exports the given certificates into the given directory as a single .p12 file +func collectAndExportIdentities(certificates []certificateutil.CertificateInfoModel, absExportOutputDirPath string, isAskForPassword bool) error { + if len(certificates) == 0 { + return nil + } + + fmt.Println() + fmt.Println() + log.Infof("Required Identities/Certificates (%d)", len(certificates)) + fmt.Println() + for _, certificate := range certificates { + log.Printf("- %s", certificate.CommonName) + } + + fmt.Println() + log.Infof("Exporting the Identities (Certificates):") + fmt.Println() + + identitiesWithKeychainRefs := []osxkeychain.IdentityWithRefModel{} + defer osxkeychain.ReleaseIdentityWithRefList(identitiesWithKeychainRefs) + + for _, certificate := range certificates { + log.Printf("searching for Identity: %s", certificate.CommonName) + identityRef, err := osxkeychain.FindAndValidateIdentity(certificate.CommonName) + if err != nil { + return fmt.Errorf("failed to export, error: %s", err) + } + + if identityRef == nil { + return errors.New("identity not found in the keychain, or it was invalid (expired)") + } + + identitiesWithKeychainRefs = append(identitiesWithKeychainRefs, *identityRef) + } + + identityKechainRefs := osxkeychain.CreateEmptyCFTypeRefSlice() + for _, aIdentityWithRefItm := range identitiesWithKeychainRefs { + fmt.Println("exporting Identity:", aIdentityWithRefItm.Label) + identityKechainRefs = append(identityKechainRefs, aIdentityWithRefItm.KeychainRef) + } + + fmt.Println() + if isAskForPassword { + log.Infof("Exporting from Keychain") + log.Warnf(" You'll be asked to provide a Passphrase for the .p12 file!") + } else { + log.Warnf("Exporting from Keychain using empty Passphrase...") + log.Printf("This means that if you want to import the file the passphrase at import should be left empty,") + log.Printf("you don't have to type in anything, just leave the passphrase input empty.") + } + fmt.Println() + log.Warnf("You'll most likely see popups one for each Identity from Keychain,") + log.Warnf("you will have to accept (Allow) those to be able to export the Identities!") + fmt.Println() + + if err := osxkeychain.ExportFromKeychain(identityKechainRefs, filepath.Join(absExportOutputDirPath, "Identities.p12"), isAskForPassword); err != nil { + return fmt.Errorf("failed to export from Keychain: %s", err) + } + + return nil +} + +// collectAndExportProvisioningProfiles copies the give profiles into the given directory +func collectAndExportProvisioningProfiles(profiles []profileutil.ProvisioningProfileInfoModel, absExportOutputDirPath string) error { + if len(profiles) == 0 { + return nil + } + + fmt.Println() + log.Infof("Required Provisioning Profiles (%d)", len(profiles)) + fmt.Println() + for _, profile := range profiles { + log.Printf("- %s (UUID: %s)", profile.Name, profile.UUID) + } + + fmt.Println() + log.Infof("Exporting Provisioning Profiles...") + fmt.Println() + + for _, profile := range profiles { + log.Printf("searching for required Provisioning Profile: %s (UUID: %s)", profile.Name, profile.UUID) + _, pth, err := profileutil.FindProvisioningProfileInfo(profile.UUID) + if err != nil { + return fmt.Errorf("failed to find Provisioning Profile: %s", err) + } + + log.Printf("file found at: %s", pth) + + exportFileName := profileExportFileName(profile, pth) + exportPth := filepath.Join(absExportOutputDirPath, exportFileName) + if err := command.RunCommand("cp", pth, exportPth); err != nil { + return fmt.Errorf("Failed to copy Provisioning Profile (from: %s) (to: %s), error: %s", pth, exportPth, err) + } + } + + return nil +} diff --git a/cmd/common_test.go b/codesigndoc/profile_test.go similarity index 94% rename from cmd/common_test.go rename to codesigndoc/profile_test.go index ad2d61f3..891cb18e 100644 --- a/cmd/common_test.go +++ b/codesigndoc/profile_test.go @@ -1,4 +1,4 @@ -package cmd +package codesigndoc import ( "testing" @@ -18,7 +18,7 @@ func timeToString(date time.Time) string { return date.Format("2006.01.02") } -func Test_filterLatestProfiles(t *testing.T) { +func TestFilterLatestProfiles(t *testing.T) { profiles := []profileutil.ProvisioningProfileInfoModel{ { Name: "Profile 1", diff --git a/codesigndoc/profiles.go b/codesigndoc/profiles.go new file mode 100644 index 00000000..0f2213e2 --- /dev/null +++ b/codesigndoc/profiles.go @@ -0,0 +1,53 @@ +package codesigndoc + +import ( + "regexp" + "strings" + + "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-tools/go-xcode/profileutil" +) + +// profileExportFileName creates a file name for the given profile with pattern: uuid.escaped_profile_name.[mobileprovision|provisionprofile] +func profileExportFileName(info profileutil.ProvisioningProfileInfoModel, path string) string { + replaceRexp, err := regexp.Compile("[^A-Za-z0-9_.-]") + if err != nil { + log.Warnf("Invalid regex, error: %s", err) + return "" + } + safeTitle := replaceRexp.ReplaceAllString(info.Name, "") + extension := ".mobileprovision" + if strings.HasSuffix(path, ".provisionprofile") { + extension = ".provisionprofile" + } + + return info.UUID + "." + safeTitle + extension +} + +// filterLatestProfiles renmoves older versions of the same profile +func filterLatestProfiles(profiles []profileutil.ProvisioningProfileInfoModel) []profileutil.ProvisioningProfileInfoModel { + profilesByBundleIDAndName := map[string][]profileutil.ProvisioningProfileInfoModel{} + for _, profile := range profiles { + bundleID := profile.BundleID + name := profile.Name + bundleIDAndName := bundleID + name + profs, ok := profilesByBundleIDAndName[bundleIDAndName] + if !ok { + profs = []profileutil.ProvisioningProfileInfoModel{} + } + profs = append(profs, profile) + profilesByBundleIDAndName[bundleIDAndName] = profs + } + + filteredProfiles := []profileutil.ProvisioningProfileInfoModel{} + for _, profiles := range profilesByBundleIDAndName { + var latestProfile *profileutil.ProvisioningProfileInfoModel + for _, profile := range profiles { + if latestProfile == nil || profile.ExpirationDate.After(latestProfile.ExpirationDate) { + latestProfile = &profile + } + } + filteredProfiles = append(filteredProfiles, *latestProfile) + } + return filteredProfiles +}