From 3e2402d94d7a4f929b9a6a2256374511bfbc331e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krisztia=CC=81n=20Go=CC=88drei?= Date: Fri, 20 Jun 2025 13:33:31 +0200 Subject: [PATCH 1/7] Move certificateutil to go-xcode@v2 --- autocodesign/autocodesign.go | 2 +- autocodesign/autocodesign_test.go | 2 +- autocodesign/certdownloader/certdownloader.go | 2 +- .../certdownloader/certdownloader_test.go | 2 +- autocodesign/certificates.go | 2 +- autocodesign/certificates_test.go | 2 +- autocodesign/codesignasset/writer.go | 2 +- .../appstoreconnectclient/certificates.go | 2 +- .../devportalclient/spaceship/certificates.go | 2 +- autocodesign/keychain/keychain.go | 2 +- autocodesign/keychain/keychain_test.go | 2 +- .../localcodesignasset_test.go | 2 +- .../localcodesignasset/profilelookup_test.go | 2 +- autocodesign/mock_AssetWriter.go | 2 +- autocodesign/mock_CertificateProvider.go | 2 +- autocodesign/utils_test.go | 2 +- certificateutil/info_model.go | 107 ++++++++++++ certificateutil/security_tool.go | 163 ++++++++++++++++++ certificateutil/util.go | 162 +++++++++++++++++ certificateutil/util_test.go | 113 ++++++++++++ codesign/codesign.go | 2 +- codesign/codesign_test.go | 2 +- exportoptionsgenerator/certificates.go | 2 +- .../exportoptionsgenerator_test.go | 2 +- go.mod | 2 +- 25 files changed, 566 insertions(+), 21 deletions(-) create mode 100644 certificateutil/info_model.go create mode 100644 certificateutil/security_tool.go create mode 100644 certificateutil/util.go create mode 100644 certificateutil/util_test.go diff --git a/autocodesign/autocodesign.go b/autocodesign/autocodesign.go index 12546aa4..1b11aa42 100644 --- a/autocodesign/autocodesign.go +++ b/autocodesign/autocodesign.go @@ -10,9 +10,9 @@ import ( "math/big" "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" + "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/bitrise-io/go-xcode/v2/devportalservice" "github.com/bitrise-io/go-xcode/xcodeproject/serialized" ) diff --git a/autocodesign/autocodesign_test.go b/autocodesign/autocodesign_test.go index 76eda4ee..eedbc4e5 100644 --- a/autocodesign/autocodesign_test.go +++ b/autocodesign/autocodesign_test.go @@ -6,9 +6,9 @@ import ( "time" "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" devportaltime "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/time" + "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" diff --git a/autocodesign/certdownloader/certdownloader.go b/autocodesign/certdownloader/certdownloader.go index fe5a988a..d5e893da 100644 --- a/autocodesign/certdownloader/certdownloader.go +++ b/autocodesign/certdownloader/certdownloader.go @@ -10,8 +10,8 @@ import ( "github.com/bitrise-io/go-steputils/input" "github.com/bitrise-io/go-utils/filedownloader" "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" + "github.com/bitrise-io/go-xcode/v2/certificateutil" ) // CertificateAndPassphrase contains a p12 file URL and passphrase diff --git a/autocodesign/certdownloader/certdownloader_test.go b/autocodesign/certdownloader/certdownloader_test.go index 627c8c74..c99f4055 100644 --- a/autocodesign/certdownloader/certdownloader_test.go +++ b/autocodesign/certdownloader/certdownloader_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/bitrise-io/go-xcode/certificateutil" + "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/stretchr/testify/assert" ) diff --git a/autocodesign/certificates.go b/autocodesign/certificates.go index b3d04028..031a0f3f 100644 --- a/autocodesign/certificates.go +++ b/autocodesign/certificates.go @@ -5,8 +5,8 @@ import ( "strings" "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" + "github.com/bitrise-io/go-xcode/v2/certificateutil" ) func selectCertificatesAndDistributionTypes(certificateSource DevPortalClient, typeToLocalCerts LocalCertificates, distribution DistributionType, signUITestTargets bool, verboseLog bool) (map[appstoreconnect.CertificateType][]Certificate, []DistributionType, error) { diff --git a/autocodesign/certificates_test.go b/autocodesign/certificates_test.go index 5e0af9e0..1ca137f9 100644 --- a/autocodesign/certificates_test.go +++ b/autocodesign/certificates_test.go @@ -8,8 +8,8 @@ import ( "time" "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" + "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) diff --git a/autocodesign/codesignasset/writer.go b/autocodesign/codesignasset/writer.go index 906c5b5e..eee8d929 100644 --- a/autocodesign/codesignasset/writer.go +++ b/autocodesign/codesignasset/writer.go @@ -8,10 +8,10 @@ import ( "github.com/bitrise-io/go-utils/log" "github.com/bitrise-io/go-utils/pathutil" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" "github.com/bitrise-io/go-xcode/v2/autocodesign/keychain" + "github.com/bitrise-io/go-xcode/v2/certificateutil" ) const ( diff --git a/autocodesign/devportalclient/appstoreconnectclient/certificates.go b/autocodesign/devportalclient/appstoreconnectclient/certificates.go index 394aa334..fcd3c401 100644 --- a/autocodesign/devportalclient/appstoreconnectclient/certificates.go +++ b/autocodesign/devportalclient/appstoreconnectclient/certificates.go @@ -7,9 +7,9 @@ import ( "math/big" "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" + "github.com/bitrise-io/go-xcode/v2/certificateutil" ) // CertificateSource ... diff --git a/autocodesign/devportalclient/spaceship/certificates.go b/autocodesign/devportalclient/spaceship/certificates.go index a15d9f05..73d1d3d8 100644 --- a/autocodesign/devportalclient/spaceship/certificates.go +++ b/autocodesign/devportalclient/spaceship/certificates.go @@ -6,9 +6,9 @@ import ( "fmt" "math/big" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" + "github.com/bitrise-io/go-xcode/v2/certificateutil" ) // CertificateSource ... diff --git a/autocodesign/keychain/keychain.go b/autocodesign/keychain/keychain.go index 63cd8ce0..e8dad26e 100644 --- a/autocodesign/keychain/keychain.go +++ b/autocodesign/keychain/keychain.go @@ -11,7 +11,7 @@ import ( "github.com/bitrise-io/go-utils/fileutil" "github.com/bitrise-io/go-utils/pathutil" "github.com/bitrise-io/go-utils/v2/command" - "github.com/bitrise-io/go-xcode/certificateutil" + "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/hashicorp/go-version" ) diff --git a/autocodesign/keychain/keychain_test.go b/autocodesign/keychain/keychain_test.go index 1928e552..0c5547fb 100644 --- a/autocodesign/keychain/keychain_test.go +++ b/autocodesign/keychain/keychain_test.go @@ -9,7 +9,7 @@ import ( "github.com/bitrise-io/go-steputils/v2/stepconf" "github.com/bitrise-io/go-utils/v2/command" "github.com/bitrise-io/go-utils/v2/env" - "github.com/bitrise-io/go-xcode/certificateutil" + "github.com/bitrise-io/go-xcode/v2/certificateutil" ) func TestCreateKeychain(t *testing.T) { diff --git a/autocodesign/localcodesignasset/localcodesignasset_test.go b/autocodesign/localcodesignasset/localcodesignasset_test.go index c017955e..6f100489 100644 --- a/autocodesign/localcodesignasset/localcodesignasset_test.go +++ b/autocodesign/localcodesignasset/localcodesignasset_test.go @@ -6,12 +6,12 @@ import ( devportaltime "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/time" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" "github.com/bitrise-io/go-xcode/v2/autocodesign/localcodesignasset/mocks" + "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) diff --git a/autocodesign/localcodesignasset/profilelookup_test.go b/autocodesign/localcodesignasset/profilelookup_test.go index 84765fb5..f388d90b 100644 --- a/autocodesign/localcodesignasset/profilelookup_test.go +++ b/autocodesign/localcodesignasset/profilelookup_test.go @@ -3,10 +3,10 @@ package localcodesignasset import ( "testing" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" + "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/stretchr/testify/assert" ) diff --git a/autocodesign/mock_AssetWriter.go b/autocodesign/mock_AssetWriter.go index d95af49f..c70f19fd 100644 --- a/autocodesign/mock_AssetWriter.go +++ b/autocodesign/mock_AssetWriter.go @@ -3,7 +3,7 @@ package autocodesign import ( - certificateutil "github.com/bitrise-io/go-xcode/certificateutil" + certificateutil "github.com/bitrise-io/go-xcode/v2/certificateutil" mock "github.com/stretchr/testify/mock" ) diff --git a/autocodesign/mock_CertificateProvider.go b/autocodesign/mock_CertificateProvider.go index 3f67bf73..b6d9347a 100644 --- a/autocodesign/mock_CertificateProvider.go +++ b/autocodesign/mock_CertificateProvider.go @@ -3,7 +3,7 @@ package autocodesign import ( - certificateutil "github.com/bitrise-io/go-xcode/certificateutil" + certificateutil "github.com/bitrise-io/go-xcode/v2/certificateutil" mock "github.com/stretchr/testify/mock" ) diff --git a/autocodesign/utils_test.go b/autocodesign/utils_test.go index 55a54949..59126157 100644 --- a/autocodesign/utils_test.go +++ b/autocodesign/utils_test.go @@ -3,9 +3,9 @@ package autocodesign import ( "testing" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/time" + "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/stretchr/testify/assert" ) diff --git a/certificateutil/info_model.go b/certificateutil/info_model.go new file mode 100644 index 00000000..fe54ede6 --- /dev/null +++ b/certificateutil/info_model.go @@ -0,0 +1,107 @@ +package certificateutil + +import ( + "crypto/rand" + "crypto/sha1" + "crypto/x509" + "errors" + "fmt" + "strings" + "time" + + "github.com/bitrise-io/go-pkcs12" + "github.com/bitrise-io/go-utils/fileutil" +) + +// CertificateInfoModel ... +type CertificateInfoModel struct { + CommonName string + TeamName string + TeamID string + EndDate time.Time + StartDate time.Time + + Serial string + SHA1Fingerprint string + + Certificate x509.Certificate + PrivateKey interface{} +} + +// NewCertificateInfo ... +func NewCertificateInfo(certificate x509.Certificate, privateKey interface{}) CertificateInfoModel { + fingerprint := sha1.Sum(certificate.Raw) + fingerprintStr := fmt.Sprintf("%x", fingerprint) + + return CertificateInfoModel{ + CommonName: certificate.Subject.CommonName, + TeamName: strings.Join(certificate.Subject.Organization, " "), + TeamID: strings.Join(certificate.Subject.OrganizationalUnit, " "), + EndDate: certificate.NotAfter, + StartDate: certificate.NotBefore, + Serial: certificate.SerialNumber.String(), + SHA1Fingerprint: fingerprintStr, + + Certificate: certificate, + PrivateKey: privateKey, + } +} + +// CertificatesFromPKCS12Content returns an array of CertificateInfoModel +// Used to parse p12 file containing multiple codesign identities (exported from macOS Keychain) +func CertificatesFromPKCS12Content(content []byte, password string) ([]CertificateInfoModel, error) { + privateKeys, certificates, err := pkcs12.DecodeAll(content, password) + if err != nil { + return nil, err + } + + if len(certificates) != len(privateKeys) { + return nil, errors.New("pkcs12: different number of certificates and private keys found") + } + + if len(certificates) == 0 { + return nil, errors.New("pkcs12: no certificate and private key pair found") + } + + infos := []CertificateInfoModel{} + for i, certificate := range certificates { + if certificate != nil { + infos = append(infos, NewCertificateInfo(*certificate, privateKeys[i])) + } + } + + return infos, nil +} + +// CertificatesFromPKCS12File ... +func CertificatesFromPKCS12File(pkcs12Pth, password string) ([]CertificateInfoModel, error) { + content, err := fileutil.ReadBytesFromFile(pkcs12Pth) + if err != nil { + return nil, err + } + + return CertificatesFromPKCS12Content(content, password) +} + +// String ... +func (info CertificateInfoModel) String() string { + team := fmt.Sprintf("%s (%s)", info.TeamName, info.TeamID) + certInfo := fmt.Sprintf("Serial: %s, Name: %s, Team: %s, Expiry: %s", info.Serial, info.CommonName, team, info.EndDate) + + err := info.CheckValidity() + if err != nil { + certInfo = certInfo + fmt.Sprintf(", error: %s", err) + } + + return certInfo +} + +// CheckValidity ... +func (info CertificateInfoModel) CheckValidity() error { + return CheckValidity(info.Certificate) +} + +// EncodeToP12 encodes a CertificateInfoModel in pkcs12 (.p12) format. +func (info CertificateInfoModel) EncodeToP12(passphrase string) ([]byte, error) { + return pkcs12.Encode(rand.Reader, info.PrivateKey, &info.Certificate, nil, passphrase) +} diff --git a/certificateutil/security_tool.go b/certificateutil/security_tool.go new file mode 100644 index 00000000..49fe4c9d --- /dev/null +++ b/certificateutil/security_tool.go @@ -0,0 +1,163 @@ +package certificateutil + +import ( + "bufio" + "crypto/x509" + "fmt" + "regexp" + "strings" + + "github.com/bitrise-io/go-utils/command" +) + +// InstalledCodesigningCertificateInfos ... +func InstalledCodesigningCertificateInfos() ([]CertificateInfoModel, error) { + certificates, err := InstalledCodesigningCertificates() + if err != nil { + return nil, err + } + + infos := []CertificateInfoModel{} + for _, certificate := range certificates { + if certificate != nil { + infos = append(infos, NewCertificateInfo(*certificate, nil)) + } + } + + return infos, nil +} + +// InstalledInstallerCertificateInfos ... +func InstalledInstallerCertificateInfos() ([]CertificateInfoModel, error) { + certificates, err := InstalledMacAppStoreCertificates() + if err != nil { + return nil, err + } + + infos := []CertificateInfoModel{} + for _, certificate := range certificates { + if certificate != nil { + infos = append(infos, NewCertificateInfo(*certificate, nil)) + } + } + + installerCertificates := FilterCertificateInfoModelsByFilterFunc(infos, func(cert CertificateInfoModel) bool { + return strings.Contains(cert.CommonName, "Installer") + }) + + return installerCertificates, nil +} + +// InstalledCodesigningCertificates ... +func InstalledCodesigningCertificates() ([]*x509.Certificate, error) { + certificateNames, err := InstalledCodesigningCertificateNames() + if err != nil { + return nil, err + } + return getInstalledCertificatesByNameSlice(certificateNames) +} + +// InstalledMacAppStoreCertificates ... +func InstalledMacAppStoreCertificates() ([]*x509.Certificate, error) { + certificateNames, err := InstalledMacAppStoreCertificateNames() + if err != nil { + return nil, err + } + return getInstalledCertificatesByNameSlice(certificateNames) +} + +// InstalledCodesigningCertificateNames ... +func InstalledCodesigningCertificateNames() ([]string, error) { + cmd := command.New("security", "find-identity", "-v", "-p", "codesigning") + out, err := cmd.RunAndReturnTrimmedCombinedOutput() + if err != nil { + return nil, commandError(cmd.PrintableCommandArgs(), out, err) + } + return installedCodesigningCertificateNamesFromOutput(out) +} + +// InstalledMacAppStoreCertificateNames ... +func InstalledMacAppStoreCertificateNames() ([]string, error) { + cmd := command.New("security", "find-identity", "-v", "-p", "macappstore") + out, err := cmd.RunAndReturnTrimmedCombinedOutput() + if err != nil { + return nil, commandError(cmd.PrintableCommandArgs(), out, err) + } + return installedCodesigningCertificateNamesFromOutput(out) +} + +func getInstalledCertificatesByNameSlice(certificateNames []string) ([]*x509.Certificate, error) { + certificates := []*x509.Certificate{} + for _, name := range certificateNames { + cmd := command.New("security", "find-certificate", "-c", name, "-p", "-a") + out, err := cmd.RunAndReturnTrimmedCombinedOutput() + if err != nil { + return nil, commandError(cmd.PrintableCommandArgs(), out, err) + } + + normalizedOuts, err := normalizeFindCertificateOut(out) + if err != nil { + return nil, err + } + + for _, normalizedOut := range normalizedOuts { + certificate, err := CeritifcateFromPemContent([]byte(normalizedOut)) + if err != nil { + return nil, err + } + + certificates = append(certificates, certificate) + } + } + + return certificates, nil +} + +func installedCodesigningCertificateNamesFromOutput(out string) ([]string, error) { + pettern := `^[0-9]+\) (?P.*) "(?P.*)"` + re := regexp.MustCompile(pettern) + + certificateNameMap := map[string]bool{} + scanner := bufio.NewScanner(strings.NewReader(out)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if matches := re.FindStringSubmatch(line); len(matches) == 3 { + name := matches[2] + certificateNameMap[name] = true + } + } + if err := scanner.Err(); err != nil { + return nil, err + } + + names := []string{} + for name := range certificateNameMap { + names = append(names, name) + } + return names, nil +} + +func normalizeFindCertificateOut(out string) ([]string, error) { + certificateContents := []string{} + pattern := `(?s)(-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)` + matches := regexp.MustCompile(pattern).FindAllString(out, -1) + if len(matches) == 0 { + return nil, fmt.Errorf("no certificates found in: %s", out) + } + + for _, certificateContent := range matches { + if !strings.HasPrefix(certificateContent, "\n") { + certificateContent = "\n" + certificateContent + } + if !strings.HasSuffix(certificateContent, "\n") { + certificateContent = certificateContent + "\n" + } + certificateContents = append(certificateContents, certificateContent) + } + + return certificateContents, nil +} + +func commandError(printableCmd string, cmdOut string, cmdErr error) error { + return fmt.Errorf("%s failed, out: %s, err: %w", printableCmd, cmdOut, cmdErr) +} diff --git a/certificateutil/util.go b/certificateutil/util.go new file mode 100644 index 00000000..9942856f --- /dev/null +++ b/certificateutil/util.go @@ -0,0 +1,162 @@ +package certificateutil + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "sort" + "time" +) + +// CertificateFromDERContent ... +func CertificateFromDERContent(content []byte) (*x509.Certificate, error) { + return x509.ParseCertificate(content) +} + +// CeritifcateFromPemContent ... +func CeritifcateFromPemContent(content []byte) (*x509.Certificate, error) { + block, _ := pem.Decode(content) + if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { + return nil, fmt.Errorf("failed to parse profile from: %s", string(content)) + } + return CertificateFromDERContent(block.Bytes) +} + +// CheckValidity ... +func CheckValidity(certificate x509.Certificate) error { + timeNow := time.Now() + if !timeNow.After(certificate.NotBefore) { + return fmt.Errorf("Certificate is not yet valid - validity starts at: %s", certificate.NotBefore) + } + if !timeNow.Before(certificate.NotAfter) { + return fmt.Errorf("Certificate is not valid anymore - validity ended at: %s", certificate.NotAfter) + } + return nil +} + +// FilterCertificateInfoModelsByFilterFunc ... +func FilterCertificateInfoModelsByFilterFunc(certificates []CertificateInfoModel, filterFunc func(certificate CertificateInfoModel) bool) []CertificateInfoModel { + filteredCertificates := []CertificateInfoModel{} + + for _, certificate := range certificates { + if filterFunc(certificate) { + filteredCertificates = append(filteredCertificates, certificate) + } + } + + return filteredCertificates +} + +// ValidCertificateInfo contains the certificate infos filtered as valid, invalid and duplicated common name certificates +type ValidCertificateInfo struct { + ValidCertificates, + InvalidCertificates, + DuplicatedCertificates []CertificateInfoModel +} + +// FilterValidCertificateInfos filters out invalid and duplicated common name certificaates +func FilterValidCertificateInfos(certificateInfos []CertificateInfoModel) ValidCertificateInfo { + var invalidCertificates []CertificateInfoModel + nameToCerts := map[string][]CertificateInfoModel{} + for _, certificateInfo := range certificateInfos { + if certificateInfo.CheckValidity() != nil { + invalidCertificates = append(invalidCertificates, certificateInfo) + continue + } + + nameToCerts[certificateInfo.CommonName] = append(nameToCerts[certificateInfo.CommonName], certificateInfo) + } + + var validCertificates, duplicatedCertificates []CertificateInfoModel + for _, certs := range nameToCerts { + if len(certs) == 0 { + continue + } + + sort.Slice(certs, func(i, j int) bool { + return certs[i].EndDate.After(certs[j].EndDate) + }) + validCertificates = append(validCertificates, certs[0]) + if len(certs) > 1 { + duplicatedCertificates = append(duplicatedCertificates, certs[1:]...) + } + } + + return ValidCertificateInfo{ + ValidCertificates: validCertificates, + InvalidCertificates: invalidCertificates, + DuplicatedCertificates: duplicatedCertificates, + } +} + +// GenerateTestCertificate creates a certificate (signed by a self-signed CA cert) for test purposes +func GenerateTestCertificate(serial int64, teamID, teamName, commonName string, expiry time.Time) (*x509.Certificate, *rsa.PrivateKey, error) { + CAtemplate := &x509.Certificate{ + IsCA: true, + BasicConstraintsValid: true, + SubjectKeyId: []byte{1, 2, 3}, + SerialNumber: big.NewInt(1234), + Subject: pkix.Name{ + Country: []string{"US"}, + Organization: []string{"Pear Worldwide Developer Relations"}, + CommonName: "Pear Worldwide Developer Relations CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + // see http://golang.org/pkg/crypto/x509/#KeyUsage + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + } + + CAprivatekey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + + // Self-signed certificate, parent is the template + CAcertData, err := x509.CreateCertificate(rand.Reader, CAtemplate, CAtemplate, &CAprivatekey.PublicKey, CAprivatekey) + if err != nil { + return nil, nil, err + } + CAcert, err := x509.ParseCertificate(CAcertData) + if err != nil { + return nil, nil, err + } + + template := &x509.Certificate{ + IsCA: true, + BasicConstraintsValid: true, + SerialNumber: big.NewInt(serial), + Subject: pkix.Name{ + Country: []string{"US"}, + Organization: []string{teamName}, + OrganizationalUnit: []string{teamID}, + CommonName: commonName, + }, + NotBefore: time.Now(), + NotAfter: expiry, + // see http://golang.org/pkg/crypto/x509/#KeyUsage + KeyUsage: x509.KeyUsageDigitalSignature, + } + + privatekey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + + certData, err := x509.CreateCertificate(rand.Reader, template, CAcert, &privatekey.PublicKey, CAprivatekey) + if err != nil { + return nil, nil, err + } + + cert, err := x509.ParseCertificate(certData) + if err != nil { + return nil, nil, err + } + + return cert, privatekey, nil +} diff --git a/certificateutil/util_test.go b/certificateutil/util_test.go new file mode 100644 index 00000000..ad5e6ece --- /dev/null +++ b/certificateutil/util_test.go @@ -0,0 +1,113 @@ +package certificateutil + +import ( + "reflect" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestFilterCertificateInfoModelsByFilterFunc(t *testing.T) { + filterableCerts := []CertificateInfoModel{ + CertificateInfoModel{TeamID: "my-team-id"}, + CertificateInfoModel{TeamID: "find-this-team-id"}, + CertificateInfoModel{TeamID: "my--another-team-id"}, + CertificateInfoModel{TeamID: "test-team-id", CommonName: "test common name"}, + CertificateInfoModel{TeamID: "test-team-id2", CommonName: "find this common name"}, + } + expectedCertsByTeamID := []CertificateInfoModel{ + CertificateInfoModel{TeamID: "find-this-team-id"}, + } + + foundCerts := FilterCertificateInfoModelsByFilterFunc(filterableCerts, func(cert CertificateInfoModel) bool { return cert.TeamID == "find-this-team-id" }) + require.Equal(t, expectedCertsByTeamID, foundCerts) + + expectedCertsByCommonNameExact := []CertificateInfoModel{ + CertificateInfoModel{TeamID: "test-team-id2", CommonName: "find this common name"}, + } + + foundCerts = FilterCertificateInfoModelsByFilterFunc(filterableCerts, func(cert CertificateInfoModel) bool { return cert.CommonName == "find this common name" }) + require.Equal(t, expectedCertsByCommonNameExact, foundCerts) + + expectedCertsByCommonNameMatch := []CertificateInfoModel{ + CertificateInfoModel{TeamID: "test-team-id", CommonName: "test common name"}, + CertificateInfoModel{TeamID: "test-team-id2", CommonName: "find this common name"}, + } + + foundCerts = FilterCertificateInfoModelsByFilterFunc(filterableCerts, func(cert CertificateInfoModel) bool { return strings.Contains(cert.CommonName, "common name") }) + require.Equal(t, expectedCertsByCommonNameMatch, foundCerts) +} + +func TestFilterValidCertificateInfos(t *testing.T) { + const serial = int64(1234) + const teamID = "MYTEAMID" + const teamName = "BITFALL FEJLESZTO KORLATOLT FELELOSSEGU TARSASAG" + const commonName = "Apple Developer: test" + validExpiry := time.Now().AddDate(1, 0, 0) + earlierValidExpiry := time.Now().AddDate(0, 1, 0) + invalidExpiry := time.Now().AddDate(-1, 0, 0) + + latestValidCert, privateKey, err := GenerateTestCertificate(serial, teamID, teamName, commonName, validExpiry) + if err != nil { + t.Errorf("init: failed to generate certificate, error: %s", err) + } + latestValidCertInfo := NewCertificateInfo(*latestValidCert, privateKey) + t.Logf("Test certificate generated: %s", latestValidCertInfo) + + earlierValidCert, privateKey, err := GenerateTestCertificate(serial, teamID, teamName, commonName, earlierValidExpiry) + if err != nil { + t.Errorf("init: failed to generate certificate, error: %s", err) + } + earlierValidCertInfo := NewCertificateInfo(*earlierValidCert, privateKey) + t.Logf("Test certificate generated: %s", earlierValidCertInfo) + + invalidCert, privateKey, err := GenerateTestCertificate(serial, teamID, teamName, commonName, invalidExpiry) + if err != nil { + t.Errorf("init: failed to generate certificate, error: %s", err) + } + invalidCertInfo := NewCertificateInfo(*invalidCert, privateKey) + t.Logf("Test certificate generated: %s", invalidCertInfo) + + tests := []struct { + name string + certificateInfos []CertificateInfoModel + want ValidCertificateInfo + }{ + { + name: "one valid cert", + certificateInfos: []CertificateInfoModel{latestValidCertInfo}, + want: ValidCertificateInfo{ + ValidCertificates: []CertificateInfoModel{latestValidCertInfo}, + InvalidCertificates: nil, + DuplicatedCertificates: nil, + }, + }, + { + name: "one valid, one invalid cert with same name", + certificateInfos: []CertificateInfoModel{latestValidCertInfo, invalidCertInfo}, + want: ValidCertificateInfo{ + ValidCertificates: []CertificateInfoModel{latestValidCertInfo}, + InvalidCertificates: []CertificateInfoModel{invalidCertInfo}, + DuplicatedCertificates: nil, + }, + }, + { + name: "2 valid, duplicated certs", + certificateInfos: []CertificateInfoModel{latestValidCertInfo, earlierValidCertInfo, invalidCertInfo}, + want: ValidCertificateInfo{ + ValidCertificates: []CertificateInfoModel{latestValidCertInfo}, + InvalidCertificates: []CertificateInfoModel{invalidCertInfo}, + DuplicatedCertificates: []CertificateInfoModel{earlierValidCertInfo}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := FilterValidCertificateInfos(tt.certificateInfos); !reflect.DeepEqual(got, tt.want) { + t.Errorf("FilterValidCertificateInfos() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/codesign/codesign.go b/codesign/codesign.go index feaeba58..929f8c3a 100644 --- a/codesign/codesign.go +++ b/codesign/codesign.go @@ -5,11 +5,11 @@ import ( "time" "github.com/bitrise-io/go-utils/v2/log" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" "github.com/bitrise-io/go-xcode/v2/autocodesign/projectmanager" + "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/bitrise-io/go-xcode/v2/devportalservice" "github.com/bitrise-io/go-xcode/v2/xcarchive" ) diff --git a/codesign/codesign_test.go b/codesign/codesign_test.go index fc585c05..fee1784e 100644 --- a/codesign/codesign_test.go +++ b/codesign/codesign_test.go @@ -10,8 +10,8 @@ import ( "github.com/bitrise-io/go-steputils/v2/stepconf" "github.com/bitrise-io/go-utils/v2/log" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" + "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/bitrise-io/go-xcode/v2/codesign/mocks" "github.com/bitrise-io/go-xcode/v2/devportalservice" "github.com/stretchr/testify/mock" diff --git a/exportoptionsgenerator/certificates.go b/exportoptionsgenerator/certificates.go index d916ce54..5b277e14 100644 --- a/exportoptionsgenerator/certificates.go +++ b/exportoptionsgenerator/certificates.go @@ -1,6 +1,6 @@ package exportoptionsgenerator -import "github.com/bitrise-io/go-xcode/certificateutil" +import "github.com/bitrise-io/go-xcode/v2/certificateutil" // CodesignIdentityProvider can list certificate infos. type CodesignIdentityProvider interface { diff --git a/exportoptionsgenerator/exportoptionsgenerator_test.go b/exportoptionsgenerator/exportoptionsgenerator_test.go index 20ade1af..7a8a31a5 100644 --- a/exportoptionsgenerator/exportoptionsgenerator_test.go +++ b/exportoptionsgenerator/exportoptionsgenerator_test.go @@ -5,10 +5,10 @@ import ( "testing" "github.com/bitrise-io/go-utils/v2/log" - "github.com/bitrise-io/go-xcode/certificateutil" "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/profileutil" + "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/bitrise-io/go-xcode/v2/exportoptionsgenerator/mocks" "github.com/bitrise-io/go-xcode/v2/xcodeversion" "github.com/stretchr/testify/require" diff --git a/go.mod b/go.mod index 99a68c18..91f56f49 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.23.2 require ( cloud.google.com/go/secretmanager v1.14.3 cloud.google.com/go/storage v1.50.0 + github.com/bitrise-io/go-pkcs12 v0.0.0-20230815095624-feb898696e02 github.com/bitrise-io/go-steputils v1.0.5 github.com/bitrise-io/go-steputils/v2 v2.0.0-alpha.18 github.com/bitrise-io/go-utils v1.0.12 @@ -35,7 +36,6 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect - github.com/bitrise-io/go-pkcs12 v0.0.0-20230815095624-feb898696e02 // indirect github.com/bitrise-io/go-plist v0.0.0-20210301100253-4b1a112ccd10 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect From 60e4288a9348fcc95a4efe082ca5e8f46a83c18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krisztia=CC=81n=20Go=CC=88drei?= Date: Fri, 20 Jun 2025 13:39:59 +0200 Subject: [PATCH 2/7] Add profileutil and export packages to go-xcode@v2 --- _integration_tests/zip/ipa_reader_test.go | 2 +- artifacts/ipa_reader.go | 2 +- autocodesign/autocodesign.go | 2 +- .../localcodesignasset_test.go | 2 +- .../mocks/ProvisioningProfileConverter.go | 2 +- .../mocks/ProvisioningProfileProvider.go | 2 +- autocodesign/localcodesignasset/profile.go | 2 +- .../localcodesignasset/profileconverter.go | 2 +- .../localcodesignasset/profilelookup.go | 2 +- .../localcodesignasset/profilelookup_test.go | 2 +- .../localcodesignasset/profileprovider.go | 2 +- .../profiledownloader/profiledownloader.go | 2 +- autocodesign/profiles.go | 2 +- export/codesing_group.go | 13 + export/export.go | 125 ++++++ export/filter.go | 203 ++++++++++ export/ios.go | 321 +++++++++++++++ export/mac.go | 73 ++++ .../exportoptionsgenerator.go | 4 +- .../exportoptionsgenerator_test.go | 2 +- exportoptionsgenerator/profiles.go | 2 +- profileutil/capabilities.go | 66 +++ profileutil/info_model.go | 254 ++++++++++++ profileutil/info_model_test.go | 203 ++++++++++ profileutil/plist_data.go | 171 ++++++++ profileutil/plist_data_test.go | 375 ++++++++++++++++++ profileutil/util.go | 110 +++++ xcarchive/ios_test.go | 2 +- 28 files changed, 1932 insertions(+), 18 deletions(-) create mode 100644 export/codesing_group.go create mode 100644 export/export.go create mode 100644 export/filter.go create mode 100644 export/ios.go create mode 100644 export/mac.go create mode 100644 profileutil/capabilities.go create mode 100644 profileutil/info_model.go create mode 100644 profileutil/info_model_test.go create mode 100644 profileutil/plist_data.go create mode 100644 profileutil/plist_data_test.go create mode 100644 profileutil/util.go diff --git a/_integration_tests/zip/ipa_reader_test.go b/_integration_tests/zip/ipa_reader_test.go index 54a20b1d..2bd783d6 100644 --- a/_integration_tests/zip/ipa_reader_test.go +++ b/_integration_tests/zip/ipa_reader_test.go @@ -7,10 +7,10 @@ import ( "github.com/bitrise-io/go-utils/v2/log" "github.com/bitrise-io/go-xcode/plistutil" - "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/_integration_tests" "github.com/bitrise-io/go-xcode/v2/artifacts" internalzip "github.com/bitrise-io/go-xcode/v2/internal/zip" + "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/bitrise-io/go-xcode/v2/zip" "github.com/stretchr/testify/require" ) diff --git a/artifacts/ipa_reader.go b/artifacts/ipa_reader.go index 0ed698ee..6493f02f 100644 --- a/artifacts/ipa_reader.go +++ b/artifacts/ipa_reader.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/bitrise-io/go-xcode/plistutil" - "github.com/bitrise-io/go-xcode/profileutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" ) // IPAReader ... diff --git a/autocodesign/autocodesign.go b/autocodesign/autocodesign.go index 1b11aa42..0da553cc 100644 --- a/autocodesign/autocodesign.go +++ b/autocodesign/autocodesign.go @@ -10,10 +10,10 @@ import ( "math/big" "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/bitrise-io/go-xcode/v2/devportalservice" + "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/bitrise-io/go-xcode/xcodeproject/serialized" ) diff --git a/autocodesign/localcodesignasset/localcodesignasset_test.go b/autocodesign/localcodesignasset/localcodesignasset_test.go index 6f100489..10bc25dc 100644 --- a/autocodesign/localcodesignasset/localcodesignasset_test.go +++ b/autocodesign/localcodesignasset/localcodesignasset_test.go @@ -7,11 +7,11 @@ import ( devportaltime "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/time" "github.com/bitrise-io/go-xcode/exportoptions" - "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" "github.com/bitrise-io/go-xcode/v2/autocodesign/localcodesignasset/mocks" "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) diff --git a/autocodesign/localcodesignasset/mocks/ProvisioningProfileConverter.go b/autocodesign/localcodesignasset/mocks/ProvisioningProfileConverter.go index c92f914d..4e56d480 100644 --- a/autocodesign/localcodesignasset/mocks/ProvisioningProfileConverter.go +++ b/autocodesign/localcodesignasset/mocks/ProvisioningProfileConverter.go @@ -7,7 +7,7 @@ import ( mock "github.com/stretchr/testify/mock" - profileutil "github.com/bitrise-io/go-xcode/profileutil" + profileutil "github.com/bitrise-io/go-xcode/v2/profileutil" ) // ProvisioningProfileConverter is an autogenerated mock type for the ProvisioningProfileConverter type diff --git a/autocodesign/localcodesignasset/mocks/ProvisioningProfileProvider.go b/autocodesign/localcodesignasset/mocks/ProvisioningProfileProvider.go index 48cafa7a..28cf2ee1 100644 --- a/autocodesign/localcodesignasset/mocks/ProvisioningProfileProvider.go +++ b/autocodesign/localcodesignasset/mocks/ProvisioningProfileProvider.go @@ -3,7 +3,7 @@ package mocks import ( - profileutil "github.com/bitrise-io/go-xcode/profileutil" + profileutil "github.com/bitrise-io/go-xcode/v2/profileutil" mock "github.com/stretchr/testify/mock" ) diff --git a/autocodesign/localcodesignasset/profile.go b/autocodesign/localcodesignasset/profile.go index c3a0789c..001fb010 100644 --- a/autocodesign/localcodesignasset/profile.go +++ b/autocodesign/localcodesignasset/profile.go @@ -1,10 +1,10 @@ package localcodesignasset import ( - "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/time" + "github.com/bitrise-io/go-xcode/v2/profileutil" ) // Profile ... diff --git a/autocodesign/localcodesignasset/profileconverter.go b/autocodesign/localcodesignasset/profileconverter.go index 30b10d9e..d7f4bc26 100644 --- a/autocodesign/localcodesignasset/profileconverter.go +++ b/autocodesign/localcodesignasset/profileconverter.go @@ -3,8 +3,8 @@ package localcodesignasset import ( "os" - "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" + "github.com/bitrise-io/go-xcode/v2/profileutil" ) // ProvisioningProfileConverter ... diff --git a/autocodesign/localcodesignasset/profilelookup.go b/autocodesign/localcodesignasset/profilelookup.go index 511b3e24..5caf88b6 100644 --- a/autocodesign/localcodesignasset/profilelookup.go +++ b/autocodesign/localcodesignasset/profilelookup.go @@ -6,8 +6,8 @@ import ( "time" "github.com/bitrise-io/go-utils/sliceutil" - "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" + "github.com/bitrise-io/go-xcode/v2/profileutil" ) func findProfile(localProfiles []profileutil.ProvisioningProfileInfoModel, platform autocodesign.Platform, distributionType autocodesign.DistributionType, bundleID string, entitlements autocodesign.Entitlements, minProfileDaysValid int, certSerials []string, deviceUDIDs []string) *profileutil.ProvisioningProfileInfoModel { diff --git a/autocodesign/localcodesignasset/profilelookup_test.go b/autocodesign/localcodesignasset/profilelookup_test.go index f388d90b..83e3aaa9 100644 --- a/autocodesign/localcodesignasset/profilelookup_test.go +++ b/autocodesign/localcodesignasset/profilelookup_test.go @@ -4,9 +4,9 @@ import ( "testing" "github.com/bitrise-io/go-xcode/exportoptions" - "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/stretchr/testify/assert" ) diff --git a/autocodesign/localcodesignasset/profileprovider.go b/autocodesign/localcodesignasset/profileprovider.go index fcdc5f9c..971ecb25 100644 --- a/autocodesign/localcodesignasset/profileprovider.go +++ b/autocodesign/localcodesignasset/profileprovider.go @@ -1,6 +1,6 @@ package localcodesignasset -import "github.com/bitrise-io/go-xcode/profileutil" +import "github.com/bitrise-io/go-xcode/v2/profileutil" // ProvisioningProfileProvider can list profile infos. type ProvisioningProfileProvider interface { diff --git a/autocodesign/profiledownloader/profiledownloader.go b/autocodesign/profiledownloader/profiledownloader.go index 3bb4210f..08c06590 100644 --- a/autocodesign/profiledownloader/profiledownloader.go +++ b/autocodesign/profiledownloader/profiledownloader.go @@ -8,9 +8,9 @@ import ( "github.com/bitrise-io/go-steputils/input" "github.com/bitrise-io/go-utils/filedownloader" - "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/autocodesign/localcodesignasset" + "github.com/bitrise-io/go-xcode/v2/profileutil" ) type downloader struct { diff --git a/autocodesign/profiles.go b/autocodesign/profiles.go index 181b09fc..708abbf9 100644 --- a/autocodesign/profiles.go +++ b/autocodesign/profiles.go @@ -10,8 +10,8 @@ import ( "github.com/bitrise-io/go-utils/log" "github.com/bitrise-io/go-utils/retry" "github.com/bitrise-io/go-utils/sliceutil" - "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" + "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/bitrise-io/go-xcode/xcodeproject/serialized" ) diff --git a/export/codesing_group.go b/export/codesing_group.go new file mode 100644 index 00000000..920e9d18 --- /dev/null +++ b/export/codesing_group.go @@ -0,0 +1,13 @@ +package export + +import ( + "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" +) + +// CodeSignGroup ... +type CodeSignGroup interface { + Certificate() certificateutil.CertificateInfoModel + InstallerCertificate() *certificateutil.CertificateInfoModel + BundleIDProfileMap() map[string]profileutil.ProvisioningProfileInfoModel +} diff --git a/export/export.go b/export/export.go new file mode 100644 index 00000000..d42c2868 --- /dev/null +++ b/export/export.go @@ -0,0 +1,125 @@ +package export + +import ( + "encoding/json" + "fmt" + "sort" + + "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" + "github.com/ryanuber/go-glob" +) + +// SelectableCodeSignGroup ... +type SelectableCodeSignGroup struct { + Certificate certificateutil.CertificateInfoModel + BundleIDProfilesMap map[string][]profileutil.ProvisioningProfileInfoModel +} + +// String ... +func (group SelectableCodeSignGroup) String() string { + printable := map[string]interface{}{} + printable["team"] = fmt.Sprintf("%s (%s)", group.Certificate.TeamName, group.Certificate.TeamID) + printable["certificate"] = fmt.Sprintf("%s (%s)", group.Certificate.CommonName, group.Certificate.Serial) + + bundleIDProfiles := map[string][]string{} + for bundleID, profileInfos := range group.BundleIDProfilesMap { + printableProfiles := []string{} + for _, profileInfo := range profileInfos { + printableProfiles = append(printableProfiles, fmt.Sprintf("%s (%s)", profileInfo.Name, profileInfo.UUID)) + } + bundleIDProfiles[bundleID] = printableProfiles + } + printable["bundle_id_profiles"] = bundleIDProfiles + + data, err := json.MarshalIndent(printable, "", "\t") + if err != nil { + log.Errorf("Failed to marshal: %v, error: %s", printable, err) + return "" + } + + return string(data) +} + +func isCertificateInstalled(installedCertificates []certificateutil.CertificateInfoModel, certificate certificateutil.CertificateInfoModel) bool { + for _, cert := range installedCertificates { + if cert.Serial == certificate.Serial { + return true + } + } + return false +} + +// CreateSelectableCodeSignGroups ... +func CreateSelectableCodeSignGroups(certificates []certificateutil.CertificateInfoModel, profiles []profileutil.ProvisioningProfileInfoModel, bundleIDs []string) []SelectableCodeSignGroup { + groups := []SelectableCodeSignGroup{} + + serialProfilesMap := map[string][]profileutil.ProvisioningProfileInfoModel{} + serialCertificateMap := map[string]certificateutil.CertificateInfoModel{} + for _, profile := range profiles { + for _, certificate := range profile.DeveloperCertificates { + if !isCertificateInstalled(certificates, certificate) { + continue + } + + certificateProfiles, ok := serialProfilesMap[certificate.Serial] + if !ok { + certificateProfiles = []profileutil.ProvisioningProfileInfoModel{} + } + certificateProfiles = append(certificateProfiles, profile) + serialProfilesMap[certificate.Serial] = certificateProfiles + serialCertificateMap[certificate.Serial] = certificate + } + } + + for serial, profiles := range serialProfilesMap { + certificate := serialCertificateMap[serial] + + bundleIDProfilesMap := map[string][]profileutil.ProvisioningProfileInfoModel{} + for _, bundleID := range bundleIDs { + + matchingProfiles := []profileutil.ProvisioningProfileInfoModel{} + for _, profile := range profiles { + if !glob.Glob(profile.BundleID, bundleID) { + continue + } + + matchingProfiles = append(matchingProfiles, profile) + } + + if len(matchingProfiles) > 0 { + sort.Sort(ByBundleIDLength(matchingProfiles)) + bundleIDProfilesMap[bundleID] = matchingProfiles + } + } + + if len(bundleIDProfilesMap) == len(bundleIDs) { + group := SelectableCodeSignGroup{ + Certificate: certificate, + BundleIDProfilesMap: bundleIDProfilesMap, + } + groups = append(groups, group) + } + } + + return groups +} + +// ByBundleIDLength ... +type ByBundleIDLength []profileutil.ProvisioningProfileInfoModel + +// Len .. +func (s ByBundleIDLength) Len() int { + return len(s) +} + +// Swap ... +func (s ByBundleIDLength) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less ... +func (s ByBundleIDLength) Less(i, j int) bool { + return len(s[i].BundleID) > len(s[j].BundleID) +} diff --git a/export/filter.go b/export/filter.go new file mode 100644 index 00000000..d20e9fb2 --- /dev/null +++ b/export/filter.go @@ -0,0 +1,203 @@ +package export + +import ( + "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-io/go-xcode/exportoptions" + "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" +) + +// SelectableCodeSignGroupFilter ... +type SelectableCodeSignGroupFilter func(group *SelectableCodeSignGroup) bool + +// FilterSelectableCodeSignGroups ... +func FilterSelectableCodeSignGroups(groups []SelectableCodeSignGroup, filterFuncs ...SelectableCodeSignGroupFilter) []SelectableCodeSignGroup { + filteredGroups := []SelectableCodeSignGroup{} + + for _, group := range groups { + allowed := true + + for _, filterFunc := range filterFuncs { + if !filterFunc(&group) { + allowed = false + break + } + } + + if allowed { + filteredGroups = append(filteredGroups, group) + } + } + + return filteredGroups +} + +// CreateEntitlementsSelectableCodeSignGroupFilter ... +func CreateEntitlementsSelectableCodeSignGroupFilter(bundleIDEntitlementsMap map[string]plistutil.PlistData) SelectableCodeSignGroupFilter { + return func(group *SelectableCodeSignGroup) bool { + log.Debugf("Entitlements filter - removes profile if has missing capabilities") + + filteredBundleIDProfilesMap := map[string][]profileutil.ProvisioningProfileInfoModel{} + + for bundleID, profiles := range group.BundleIDProfilesMap { + filteredProfiles := []profileutil.ProvisioningProfileInfoModel{} + + for _, profile := range profiles { + missingEntitlements := profileutil.MatchTargetAndProfileEntitlements(bundleIDEntitlementsMap[bundleID], profile.Entitlements, profile.Type) + if len(missingEntitlements) == 0 { + filteredProfiles = append(filteredProfiles, profile) + } + } + + if len(filteredProfiles) == 0 { + break + } + + filteredBundleIDProfilesMap[bundleID] = filteredProfiles + } + + if len(filteredBundleIDProfilesMap) == len(group.BundleIDProfilesMap) { + group.BundleIDProfilesMap = filteredBundleIDProfilesMap + return true + } + + return false + } +} + +// CreateExportMethodSelectableCodeSignGroupFilter ... +func CreateExportMethodSelectableCodeSignGroupFilter(exportMethod exportoptions.Method) SelectableCodeSignGroupFilter { + return func(group *SelectableCodeSignGroup) bool { + log.Debugf("Export method filter - removes profile if distribution type is not: %s", exportMethod) + + filteredBundleIDProfilesMap := map[string][]profileutil.ProvisioningProfileInfoModel{} + + for bundleID, profiles := range group.BundleIDProfilesMap { + filteredProfiles := []profileutil.ProvisioningProfileInfoModel{} + + for _, profile := range profiles { + if profile.ExportType == exportMethod { + filteredProfiles = append(filteredProfiles, profile) + } + } + + if len(filteredProfiles) == 0 { + break + } + + filteredBundleIDProfilesMap[bundleID] = filteredProfiles + } + + if len(filteredBundleIDProfilesMap) == len(group.BundleIDProfilesMap) { + group.BundleIDProfilesMap = filteredBundleIDProfilesMap + return true + } + + return false + } +} + +// CreateTeamSelectableCodeSignGroupFilter ... +func CreateTeamSelectableCodeSignGroupFilter(teamID string) SelectableCodeSignGroupFilter { + return func(group *SelectableCodeSignGroup) bool { + log.Debugf("Development Team filter - restrict group if team is not: %s", teamID) + + return group.Certificate.TeamID == teamID + } +} + +// CreateNotXcodeManagedSelectableCodeSignGroupFilter ... +func CreateNotXcodeManagedSelectableCodeSignGroupFilter() SelectableCodeSignGroupFilter { + return func(group *SelectableCodeSignGroup) bool { + log.Debugf("Xcode managed filter - removes profile if xcode managed") + + filteredBundleIDProfilesMap := map[string][]profileutil.ProvisioningProfileInfoModel{} + + for bundleID, profiles := range group.BundleIDProfilesMap { + filteredProfiles := []profileutil.ProvisioningProfileInfoModel{} + + for _, profile := range profiles { + if !profile.IsXcodeManaged() { + filteredProfiles = append(filteredProfiles, profile) + } + } + + if len(filteredProfiles) == 0 { + break + } + + filteredBundleIDProfilesMap[bundleID] = filteredProfiles + } + + if len(filteredBundleIDProfilesMap) == len(group.BundleIDProfilesMap) { + group.BundleIDProfilesMap = filteredBundleIDProfilesMap + return true + } + + return false + } +} + +// CreateXcodeManagedSelectableCodeSignGroupFilter ... +func CreateXcodeManagedSelectableCodeSignGroupFilter() SelectableCodeSignGroupFilter { + return func(group *SelectableCodeSignGroup) bool { + log.Debugf("Xcode managed filter - removes profile if not xcode managed") + + filteredBundleIDProfilesMap := map[string][]profileutil.ProvisioningProfileInfoModel{} + + for bundleID, profiles := range group.BundleIDProfilesMap { + filteredProfiles := []profileutil.ProvisioningProfileInfoModel{} + + for _, profile := range profiles { + if profile.IsXcodeManaged() { + filteredProfiles = append(filteredProfiles, profile) + } + } + + if len(filteredProfiles) == 0 { + break + } + + filteredBundleIDProfilesMap[bundleID] = filteredProfiles + } + + if len(filteredBundleIDProfilesMap) == len(group.BundleIDProfilesMap) { + group.BundleIDProfilesMap = filteredBundleIDProfilesMap + return true + } + + return false + } +} + +// CreateExcludeProfileNameSelectableCodeSignGroupFilter ... +func CreateExcludeProfileNameSelectableCodeSignGroupFilter(name string) SelectableCodeSignGroupFilter { + return func(group *SelectableCodeSignGroup) bool { + log.Debugf("Profile name filter - removes profile with name: %s", name) + + filteredBundleIDProfilesMap := map[string][]profileutil.ProvisioningProfileInfoModel{} + + for bundleID, profiles := range group.BundleIDProfilesMap { + filteredProfiles := []profileutil.ProvisioningProfileInfoModel{} + + for _, profile := range profiles { + if profile.Name != name { + filteredProfiles = append(filteredProfiles, profile) + } + } + + if len(filteredProfiles) == 0 { + break + } + + filteredBundleIDProfilesMap[bundleID] = filteredProfiles + } + + if len(filteredBundleIDProfilesMap) == len(group.BundleIDProfilesMap) { + group.BundleIDProfilesMap = filteredBundleIDProfilesMap + return true + } + + return false + } +} diff --git a/export/ios.go b/export/ios.go new file mode 100644 index 00000000..c97bf03e --- /dev/null +++ b/export/ios.go @@ -0,0 +1,321 @@ +package export + +import ( + "sort" + + "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" + glob "github.com/ryanuber/go-glob" +) + +// IosCodeSignGroup ... +type IosCodeSignGroup struct { + certificate certificateutil.CertificateInfoModel + bundleIDProfileMap map[string]profileutil.ProvisioningProfileInfoModel +} + +// Certificate ... +func (signGroup *IosCodeSignGroup) Certificate() certificateutil.CertificateInfoModel { + return signGroup.certificate +} + +// InstallerCertificate ... +func (signGroup *IosCodeSignGroup) InstallerCertificate() *certificateutil.CertificateInfoModel { + return nil +} + +// BundleIDProfileMap ... +func (signGroup *IosCodeSignGroup) BundleIDProfileMap() map[string]profileutil.ProvisioningProfileInfoModel { + return signGroup.bundleIDProfileMap +} + +// NewIOSGroup ... +func NewIOSGroup(certificate certificateutil.CertificateInfoModel, bundleIDProfileMap map[string]profileutil.ProvisioningProfileInfoModel) *IosCodeSignGroup { + return &IosCodeSignGroup{ + certificate: certificate, + bundleIDProfileMap: bundleIDProfileMap, + } +} + +func createSingleWildcardGroups(group SelectableCodeSignGroup, alreadyUsedProfileUUIDMap map[string]bool) []IosCodeSignGroup { + groups := []IosCodeSignGroup{} + + certificate := group.Certificate + bundleIDProfilesMap := group.BundleIDProfilesMap + + bundleIDs := []string{} + profiles := []profileutil.ProvisioningProfileInfoModel{} + for bundleID, matchingProfiles := range bundleIDProfilesMap { + bundleIDs = append(bundleIDs, bundleID) + profiles = append(profiles, matchingProfiles...) + } + + for _, profile := range profiles { + if alreadyUsedProfileUUIDMap[profile.UUID] { + continue + } + + matchesForAllBundleID := true + for _, bundleID := range bundleIDs { + if !glob.Glob(profile.BundleID, bundleID) { + matchesForAllBundleID = false + break + } + } + if matchesForAllBundleID { + bundleIDProfileMap := map[string]profileutil.ProvisioningProfileInfoModel{} + for _, bundleID := range bundleIDs { + bundleIDProfileMap[bundleID] = profile + } + + group := IosCodeSignGroup{ + certificate: certificate, + bundleIDProfileMap: bundleIDProfileMap, + } + groups = append(groups, group) + + alreadyUsedProfileUUIDMap[profile.UUID] = true + } + } + return groups +} + +func createXcodeManagedGroups(group SelectableCodeSignGroup, alreadyUsedProfileUUIDMap map[string]bool) []IosCodeSignGroup { + groups := []IosCodeSignGroup{} + + certificate := group.Certificate + bundleIDProfilesMap := group.BundleIDProfilesMap + + bundleIDs := []string{} + profiles := []profileutil.ProvisioningProfileInfoModel{} + for bundleID, matchingProfiles := range bundleIDProfilesMap { + bundleIDs = append(bundleIDs, bundleID) + profiles = append(profiles, matchingProfiles...) + } + + // collect xcode managed profiles + xcodeManagedProfiles := []profileutil.ProvisioningProfileInfoModel{} + for _, profile := range profiles { + if !alreadyUsedProfileUUIDMap[profile.UUID] && profile.IsXcodeManaged() { + xcodeManagedProfiles = append(xcodeManagedProfiles, profile) + } + } + sort.Sort(ByBundleIDLength(xcodeManagedProfiles)) + + // map profiles to bundle ids + remove the already used profiles + bundleIDMannagedProfilesMap := map[string][]profileutil.ProvisioningProfileInfoModel{} + for _, bundleID := range bundleIDs { + for _, profile := range xcodeManagedProfiles { + if !glob.Glob(profile.BundleID, bundleID) { + continue + } + + matchingProfiles := bundleIDMannagedProfilesMap[bundleID] + if matchingProfiles == nil { + matchingProfiles = []profileutil.ProvisioningProfileInfoModel{} + } + matchingProfiles = append(matchingProfiles, profile) + bundleIDMannagedProfilesMap[bundleID] = matchingProfiles + } + } + + if len(bundleIDMannagedProfilesMap) == len(bundleIDs) { + // if only one profile can sign a bundle id, remove it from bundleIDMannagedProfilesMap + alreadyUsedManagedProfileMap := map[string]bool{} + for _, profiles := range bundleIDMannagedProfilesMap { + if len(profiles) == 1 { + profile := profiles[0] + alreadyUsedManagedProfileMap[profile.UUID] = true + } + } + + bundleIDMannagedProfileMap := map[string]profileutil.ProvisioningProfileInfoModel{} + for bundleID, profiles := range bundleIDMannagedProfilesMap { + if len(profiles) == 1 { + bundleIDMannagedProfileMap[bundleID] = profiles[0] + } else { + remainingProfiles := []profileutil.ProvisioningProfileInfoModel{} + for _, profile := range profiles { + if !alreadyUsedManagedProfileMap[profile.UUID] { + remainingProfiles = append(remainingProfiles, profile) + } + } + if len(remainingProfiles) == 1 { + bundleIDMannagedProfileMap[bundleID] = remainingProfiles[0] + } + } + } + + // create code sign group + if len(bundleIDMannagedProfileMap) == len(bundleIDs) { + for _, profile := range bundleIDMannagedProfileMap { + alreadyUsedProfileUUIDMap[profile.UUID] = true + } + + group := IosCodeSignGroup{ + certificate: certificate, + bundleIDProfileMap: bundleIDMannagedProfileMap, + } + groups = append(groups, group) + } + } + + return groups +} + +func createNotXcodeManagedGroups(group SelectableCodeSignGroup, alreadyUsedProfileUUIDMap map[string]bool) []IosCodeSignGroup { + groups := []IosCodeSignGroup{} + + certificate := group.Certificate + bundleIDProfilesMap := group.BundleIDProfilesMap + + bundleIDs := []string{} + profiles := []profileutil.ProvisioningProfileInfoModel{} + for bundleID, matchingProfiles := range bundleIDProfilesMap { + bundleIDs = append(bundleIDs, bundleID) + profiles = append(profiles, matchingProfiles...) + } + + // collect xcode managed profiles + notXcodeManagedProfiles := []profileutil.ProvisioningProfileInfoModel{} + for _, profile := range profiles { + if !alreadyUsedProfileUUIDMap[profile.UUID] && !profile.IsXcodeManaged() { + notXcodeManagedProfiles = append(notXcodeManagedProfiles, profile) + } + } + sort.Sort(ByBundleIDLength(notXcodeManagedProfiles)) + + // map profiles to bundle ids + remove the already used profiles + bundleIDNotMannagedProfilesMap := map[string][]profileutil.ProvisioningProfileInfoModel{} + for _, bundleID := range bundleIDs { + for _, profile := range notXcodeManagedProfiles { + if !glob.Glob(profile.BundleID, bundleID) { + continue + } + + matchingProfiles := bundleIDNotMannagedProfilesMap[bundleID] + if matchingProfiles == nil { + matchingProfiles = []profileutil.ProvisioningProfileInfoModel{} + } + matchingProfiles = append(matchingProfiles, profile) + bundleIDNotMannagedProfilesMap[bundleID] = matchingProfiles + } + } + + if len(bundleIDNotMannagedProfilesMap) == len(bundleIDs) { + // if only one profile can sign a bundle id, remove it from bundleIDNotMannagedProfilesMap + alreadyUsedNotManagedProfileMap := map[string]bool{} + for _, profiles := range bundleIDNotMannagedProfilesMap { + if len(profiles) == 1 { + profile := profiles[0] + alreadyUsedNotManagedProfileMap[profile.UUID] = true + } + } + + bundleIDNotMannagedProfileMap := map[string]profileutil.ProvisioningProfileInfoModel{} + for bundleID, profiles := range bundleIDNotMannagedProfilesMap { + if len(profiles) == 1 { + bundleIDNotMannagedProfileMap[bundleID] = profiles[0] + } else { + remainingProfiles := []profileutil.ProvisioningProfileInfoModel{} + for _, profile := range profiles { + if !alreadyUsedNotManagedProfileMap[profile.UUID] { + remainingProfiles = append(remainingProfiles, profile) + } + } + if len(remainingProfiles) == 1 { + bundleIDNotMannagedProfileMap[bundleID] = remainingProfiles[0] + } + } + } + + // create code sign group + if len(bundleIDNotMannagedProfileMap) == len(bundleIDs) { + for _, profile := range bundleIDNotMannagedProfileMap { + alreadyUsedProfileUUIDMap[profile.UUID] = true + } + + codeSignGroup := IosCodeSignGroup{ + certificate: certificate, + bundleIDProfileMap: bundleIDNotMannagedProfileMap, + } + groups = append(groups, codeSignGroup) + } + } + + return groups +} + +func createRemainingGroups(group SelectableCodeSignGroup, alreadyUsedProfileUUIDMap map[string]bool) []IosCodeSignGroup { + groups := []IosCodeSignGroup{} + + certificate := group.Certificate + bundleIDProfilesMap := group.BundleIDProfilesMap + + bundleIDs := []string{} + profiles := []profileutil.ProvisioningProfileInfoModel{} + for bundleID, matchingProfiles := range bundleIDProfilesMap { + bundleIDs = append(bundleIDs, bundleID) + profiles = append(profiles, matchingProfiles...) + } + + if len(alreadyUsedProfileUUIDMap) != len(profiles) { + bundleIDProfileMap := map[string]profileutil.ProvisioningProfileInfoModel{} + for _, bundleID := range bundleIDs { + for _, profile := range profiles { + if alreadyUsedProfileUUIDMap[profile.UUID] { + continue + } + + if !glob.Glob(profile.BundleID, bundleID) { + continue + } + + bundleIDProfileMap[bundleID] = profile + break + } + } + + if len(bundleIDProfileMap) == len(bundleIDs) { + group := IosCodeSignGroup{ + certificate: certificate, + bundleIDProfileMap: bundleIDProfileMap, + } + groups = append(groups, group) + } + } + + return groups +} + +// CreateIosCodeSignGroups ... +func CreateIosCodeSignGroups(selectableGroups []SelectableCodeSignGroup) []IosCodeSignGroup { + alreadyUsedProfileUUIDMap := map[string]bool{} + + singleWildcardGroups := []IosCodeSignGroup{} + xcodeManagedGroups := []IosCodeSignGroup{} + notXcodeManagedGroups := []IosCodeSignGroup{} + remainingGroups := []IosCodeSignGroup{} + + for _, selectableGroup := range selectableGroups { + // create groups with single wildcard profiles + singleWildcardGroups = append(singleWildcardGroups, createSingleWildcardGroups(selectableGroup, alreadyUsedProfileUUIDMap)...) + + // create groups with xcode managed profiles + xcodeManagedGroups = append(xcodeManagedGroups, createXcodeManagedGroups(selectableGroup, alreadyUsedProfileUUIDMap)...) + + // create groups with NOT xcode managed profiles + notXcodeManagedGroups = append(notXcodeManagedGroups, createNotXcodeManagedGroups(selectableGroup, alreadyUsedProfileUUIDMap)...) + + // if there are remaining profiles we create a not exact group by using the first matching profile for every bundle id + remainingGroups = append(remainingGroups, createRemainingGroups(selectableGroup, alreadyUsedProfileUUIDMap)...) + } + + codeSignGroups := []IosCodeSignGroup{} + codeSignGroups = append(codeSignGroups, notXcodeManagedGroups...) + codeSignGroups = append(codeSignGroups, xcodeManagedGroups...) + codeSignGroups = append(codeSignGroups, singleWildcardGroups...) + codeSignGroups = append(codeSignGroups, remainingGroups...) + + return codeSignGroups +} diff --git a/export/mac.go b/export/mac.go new file mode 100644 index 00000000..b2ebb063 --- /dev/null +++ b/export/mac.go @@ -0,0 +1,73 @@ +package export + +import ( + "github.com/bitrise-io/go-xcode/exportoptions" + "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" +) + +// MacCodeSignGroup ... +type MacCodeSignGroup struct { + certificate certificateutil.CertificateInfoModel + installerCertificate *certificateutil.CertificateInfoModel + bundleIDProfileMap map[string]profileutil.ProvisioningProfileInfoModel +} + +// Certificate ... +func (signGroup *MacCodeSignGroup) Certificate() certificateutil.CertificateInfoModel { + return signGroup.certificate +} + +// InstallerCertificate ... +func (signGroup *MacCodeSignGroup) InstallerCertificate() *certificateutil.CertificateInfoModel { + return signGroup.installerCertificate +} + +// BundleIDProfileMap ... +func (signGroup *MacCodeSignGroup) BundleIDProfileMap() map[string]profileutil.ProvisioningProfileInfoModel { + return signGroup.bundleIDProfileMap +} + +// NewMacGroup ... +func NewMacGroup(certificate certificateutil.CertificateInfoModel, installerCertificate *certificateutil.CertificateInfoModel, bundleIDProfileMap map[string]profileutil.ProvisioningProfileInfoModel) *MacCodeSignGroup { + return &MacCodeSignGroup{ + certificate: certificate, + installerCertificate: installerCertificate, + bundleIDProfileMap: bundleIDProfileMap, + } +} + +// CreateMacCodeSignGroup ... +func CreateMacCodeSignGroup(selectableGroups []SelectableCodeSignGroup, installedInstallerCertificates []certificateutil.CertificateInfoModel, exportMethod exportoptions.Method) []MacCodeSignGroup { + macosCodeSignGroups := []MacCodeSignGroup{} + + iosCodesignGroups := CreateIosCodeSignGroups(selectableGroups) + + for _, group := range iosCodesignGroups { + if exportMethod.IsAppStore() { + installerCertificates := []certificateutil.CertificateInfoModel{} + + for _, installerCertificate := range installedInstallerCertificates { + if installerCertificate.TeamID == group.certificate.TeamID { + installerCertificates = append(installerCertificates, installerCertificate) + } + } + + if len(installerCertificates) > 0 { + installerCertificate := installerCertificates[0] + macosCodeSignGroups = append(macosCodeSignGroups, MacCodeSignGroup{ + certificate: group.certificate, + installerCertificate: &installerCertificate, + bundleIDProfileMap: group.bundleIDProfileMap, + }) + } + } else { + macosCodeSignGroups = append(macosCodeSignGroups, MacCodeSignGroup{ + certificate: group.certificate, + bundleIDProfileMap: group.bundleIDProfileMap, + }) + } + } + + return macosCodeSignGroups +} diff --git a/exportoptionsgenerator/exportoptionsgenerator.go b/exportoptionsgenerator/exportoptionsgenerator.go index feb6bf4a..c8536130 100644 --- a/exportoptionsgenerator/exportoptionsgenerator.go +++ b/exportoptionsgenerator/exportoptionsgenerator.go @@ -6,10 +6,10 @@ import ( "github.com/bitrise-io/go-utils/sliceutil" "github.com/bitrise-io/go-utils/v2/log" - "github.com/bitrise-io/go-xcode/export" "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/plistutil" - "github.com/bitrise-io/go-xcode/profileutil" + "github.com/bitrise-io/go-xcode/v2/export" + "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/bitrise-io/go-xcode/v2/xcodeversion" ) diff --git a/exportoptionsgenerator/exportoptionsgenerator_test.go b/exportoptionsgenerator/exportoptionsgenerator_test.go index 7a8a31a5..a9ec9d45 100644 --- a/exportoptionsgenerator/exportoptionsgenerator_test.go +++ b/exportoptionsgenerator/exportoptionsgenerator_test.go @@ -7,9 +7,9 @@ import ( "github.com/bitrise-io/go-utils/v2/log" "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/plistutil" - "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/bitrise-io/go-xcode/v2/exportoptionsgenerator/mocks" + "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/bitrise-io/go-xcode/v2/xcodeversion" "github.com/stretchr/testify/require" ) diff --git a/exportoptionsgenerator/profiles.go b/exportoptionsgenerator/profiles.go index e9b3928e..30dab9a4 100644 --- a/exportoptionsgenerator/profiles.go +++ b/exportoptionsgenerator/profiles.go @@ -8,7 +8,7 @@ import ( "github.com/bitrise-io/go-utils/pathutil" "github.com/bitrise-io/go-utils/v2/log" - "github.com/bitrise-io/go-xcode/profileutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" ) // ProvisioningProfileProvider can list profile infos. diff --git a/profileutil/capabilities.go b/profileutil/capabilities.go new file mode 100644 index 00000000..3e5b850b --- /dev/null +++ b/profileutil/capabilities.go @@ -0,0 +1,66 @@ +package profileutil + +import ( + "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-io/go-xcode/plistutil" +) + +// MatchTargetAndProfileEntitlements ... +func MatchTargetAndProfileEntitlements(targetEntitlements plistutil.PlistData, profileEntitlements plistutil.PlistData, profileType ProfileType) []string { + missingEntitlements := []string{} + + for key := range targetEntitlements { + _, known := KnownProfileCapabilitiesMap[profileType][key] + if !known { + continue + } + _, found := profileEntitlements[key] + if !found { + missingEntitlements = append(missingEntitlements, key) + } + } + + log.Debugf("Found %v entitlements from %v target", len(missingEntitlements), len(targetEntitlements)) + + return missingEntitlements +} + +// KnownProfileCapabilitiesMap ... +var KnownProfileCapabilitiesMap = map[ProfileType]map[string]bool{ + ProfileTypeMacOs: map[string]bool{ + "com.apple.developer.networking.networkextension": true, + "com.apple.developer.icloud-container-environment": true, + "com.apple.developer.icloud-container-development-container-identifiers": true, + "com.apple.developer.aps-environment": true, + "keychain-access-groups": true, + "com.apple.developer.icloud-services": true, + "com.apple.developer.icloud-container-identifiers": true, + "com.apple.developer.networking.vpn.api": true, + "com.apple.developer.ubiquity-kvstore-identifier": true, + "com.apple.developer.ubiquity-container-identifiers": true, + "com.apple.developer.game-center": true, + "com.apple.application-identifier": true, + "com.apple.developer.team-identifier": true, + "com.apple.developer.maps": true, + }, + ProfileTypeIos: map[string]bool{ + "com.apple.developer.in-app-payments": true, + "com.apple.security.application-groups": true, + "com.apple.developer.default-data-protection": true, + "com.apple.developer.healthkit": true, + "com.apple.developer.homekit": true, + "com.apple.developer.networking.HotspotConfiguration": true, + "inter-app-audio": true, + "keychain-access-groups": true, + "com.apple.developer.networking.multipath": true, + "com.apple.developer.nfc.readersession.formats": true, + "com.apple.developer.networking.networkextension": true, + "aps-environment": true, + "com.apple.developer.associated-domains": true, + "com.apple.developer.siri": true, + "com.apple.developer.networking.vpn.api": true, + "com.apple.external-accessory.wireless-configuration": true, + "com.apple.developer.pass-type-identifiers": true, + "com.apple.developer.icloud-container-identifiers": true, + }, +} diff --git a/profileutil/info_model.go b/profileutil/info_model.go new file mode 100644 index 00000000..36c87230 --- /dev/null +++ b/profileutil/info_model.go @@ -0,0 +1,254 @@ +package profileutil + +import ( + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-io/go-xcode/exportoptions" + "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/fullsailor/pkcs7" + "howett.net/plist" +) + +// ProvisioningProfileInfoModel ... +type ProvisioningProfileInfoModel struct { + UUID string + Name string + TeamName string + TeamID string + BundleID string + ExportType exportoptions.Method + ProvisionedDevices []string + DeveloperCertificates []certificateutil.CertificateInfoModel + CreationDate time.Time + ExpirationDate time.Time + Entitlements plistutil.PlistData + ProvisionsAllDevices bool + Type ProfileType +} + +func collectCapabilitesPrintableInfo(entitlements plistutil.PlistData) map[string]interface{} { + capabilities := map[string]interface{}{} + + for key, value := range entitlements { + if KnownProfileCapabilitiesMap[ProfileTypeIos][key] || + KnownProfileCapabilitiesMap[ProfileTypeMacOs][key] { + capabilities[key] = value + } + } + + return capabilities +} + +// PrintableProvisioningProfileInfo ... +func (info ProvisioningProfileInfoModel) String(installedCertificates ...certificateutil.CertificateInfoModel) string { + printable := map[string]interface{}{} + printable["name"] = fmt.Sprintf("%s (%s)", info.Name, info.UUID) + printable["export_type"] = string(info.ExportType) + printable["team"] = fmt.Sprintf("%s (%s)", info.TeamName, info.TeamID) + printable["bundle_id"] = info.BundleID + printable["expiry"] = info.ExpirationDate.String() + printable["is_xcode_managed"] = info.IsXcodeManaged() + + printable["capabilities"] = collectCapabilitesPrintableInfo(info.Entitlements) + + if info.ProvisionedDevices != nil { + printable["devices"] = info.ProvisionedDevices + } + + certificates := []map[string]interface{}{} + for _, certificateInfo := range info.DeveloperCertificates { + certificate := map[string]interface{}{} + certificate["name"] = certificateInfo.CommonName + certificate["serial"] = certificateInfo.Serial + certificate["team_id"] = certificateInfo.TeamID + certificates = append(certificates, certificate) + } + printable["certificates"] = certificates + + errors := []string{} + if installedCertificates != nil && !info.HasInstalledCertificate(installedCertificates) { + errors = append(errors, "none of the profile's certificates are installed") + } + + if err := info.CheckValidity(); err != nil { + errors = append(errors, err.Error()) + } + if len(errors) > 0 { + printable["errors"] = errors + } + + data, err := json.MarshalIndent(printable, "", "\t") + if err != nil { + log.Errorf("Failed to marshal: %v, error: %s", printable, err) + return "" + } + + return string(data) +} + +// IsXcodeManaged ... +func IsXcodeManaged(profileName string) bool { + if strings.HasPrefix(profileName, "XC") { + return true + } + if strings.Contains(profileName, "Provisioning Profile") { + if strings.HasPrefix(profileName, "iOS Team") || + strings.HasPrefix(profileName, "Mac Catalyst Team") || + strings.HasPrefix(profileName, "tvOS Team") || + strings.HasPrefix(profileName, "Mac Team") { + return true + } + } + return false +} + +// IsXcodeManaged ... +func (info ProvisioningProfileInfoModel) IsXcodeManaged() bool { + return IsXcodeManaged(info.Name) +} + +// CheckValidity ... +func (info ProvisioningProfileInfoModel) CheckValidity() error { + timeNow := time.Now() + if !timeNow.Before(info.ExpirationDate) { + return fmt.Errorf("Provisioning Profile is not valid anymore - validity ended at: %s", info.ExpirationDate) + } + return nil +} + +// HasInstalledCertificate ... +func (info ProvisioningProfileInfoModel) HasInstalledCertificate(installedCertificates []certificateutil.CertificateInfoModel) bool { + has := false + for _, certificate := range info.DeveloperCertificates { + for _, installedCertificate := range installedCertificates { + if certificate.Serial == installedCertificate.Serial { + has = true + break + } + } + } + return has +} + +// NewProvisioningProfileInfo ... +func NewProvisioningProfileInfo(provisioningProfile pkcs7.PKCS7) (ProvisioningProfileInfoModel, error) { + var data plistutil.PlistData + if _, err := plist.Unmarshal(provisioningProfile.Content, &data); err != nil { + return ProvisioningProfileInfoModel{}, err + } + + platforms, _ := data.GetStringArray("Platform") + if len(platforms) == 0 { + return ProvisioningProfileInfoModel{}, fmt.Errorf("missing Platform array in profile") + } + + platform := strings.ToLower(platforms[0]) + var profileType ProfileType + + switch platform { + case string(ProfileTypeIos): + profileType = ProfileTypeIos + case string(ProfileTypeMacOs): + profileType = ProfileTypeMacOs + case string(ProfileTypeTvOs): + profileType = ProfileTypeTvOs + default: + return ProvisioningProfileInfoModel{}, fmt.Errorf("unknown platform type: %s", platform) + } + + profile := PlistData(data) + info := ProvisioningProfileInfoModel{ + UUID: profile.GetUUID(), + Name: profile.GetName(), + TeamName: profile.GetTeamName(), + TeamID: profile.GetTeamID(), + BundleID: profile.GetBundleIdentifier(), + CreationDate: profile.GetCreationDate(), + ExpirationDate: profile.GetExpirationDate(), + ProvisionsAllDevices: profile.GetProvisionsAllDevices(), + Type: profileType, + } + + info.ExportType = profile.GetExportMethod() + + if devicesList := profile.GetProvisionedDevices(); devicesList != nil { + info.ProvisionedDevices = devicesList + } + + developerCertificates, found := data.GetByteArrayArray("DeveloperCertificates") + if found { + certificates := []*x509.Certificate{} + for _, certificateBytes := range developerCertificates { + certificate, err := certificateutil.CertificateFromDERContent(certificateBytes) + if err == nil && certificate != nil { + certificates = append(certificates, certificate) + } + } + + for _, certificate := range certificates { + if certificate != nil { + info.DeveloperCertificates = append(info.DeveloperCertificates, certificateutil.NewCertificateInfo(*certificate, nil)) + } + } + } + + info.Entitlements = profile.GetEntitlements() + + return info, nil +} + +// NewProvisioningProfileInfoFromFile ... +func NewProvisioningProfileInfoFromFile(pth string) (ProvisioningProfileInfoModel, error) { + provisioningProfile, err := ProvisioningProfileFromFile(pth) + if err != nil { + return ProvisioningProfileInfoModel{}, err + } + if provisioningProfile != nil { + return NewProvisioningProfileInfo(*provisioningProfile) + } + return ProvisioningProfileInfoModel{}, errors.New("failed to parse provisioning profile infos") +} + +// InstalledProvisioningProfileInfos ... +func InstalledProvisioningProfileInfos(profileType ProfileType) ([]ProvisioningProfileInfoModel, error) { + provisioningProfiles, err := InstalledProvisioningProfiles(profileType) + if err != nil { + return nil, err + } + + infos := []ProvisioningProfileInfoModel{} + for _, provisioningProfile := range provisioningProfiles { + if provisioningProfile != nil { + info, err := NewProvisioningProfileInfo(*provisioningProfile) + if err != nil { + return nil, err + } + infos = append(infos, info) + } + } + return infos, nil +} + +// FindProvisioningProfileInfo ... +func FindProvisioningProfileInfo(uuid string) (ProvisioningProfileInfoModel, string, error) { + profile, pth, err := FindProvisioningProfile(uuid) + if err != nil { + return ProvisioningProfileInfoModel{}, "", err + } + if pth == "" || profile == nil { + return ProvisioningProfileInfoModel{}, "", nil + } + + info, err := NewProvisioningProfileInfo(*profile) + if err != nil { + return ProvisioningProfileInfoModel{}, "", err + } + return info, pth, nil +} diff --git a/profileutil/info_model_test.go b/profileutil/info_model_test.go new file mode 100644 index 00000000..4ca3a4c5 --- /dev/null +++ b/profileutil/info_model_test.go @@ -0,0 +1,203 @@ +package profileutil + +import ( + "testing" + + "github.com/fullsailor/pkcs7" + + "github.com/stretchr/testify/require" +) + +func TestIsXcodeManaged(t *testing.T) { + xcodeManagedNames := []string{ + "XC iOS: custom.bundle.id", + "XC tvOS: custom.bundle.id", + "iOS Team Provisioning Profile: another.custom.bundle.id", + "tvOS Team Provisioning Profile: another.custom.bundle.id", + "iOS Team Store Provisioning Profile: my.bundle.id", + "tvOS Team Store Provisioning Profile: my.bundle.id", + "Mac Team Provisioning Profile: my.bundle.id", + "Mac Team Store Provisioning Profile: my.bundle.id", + "Mac Catalyst Team Provisioning Profile: my.bundle.id", + } + nonXcodeManagedNames := []string{ + "Test Profile Name", + "iOS Distribution Profile: test.bundle.id", + "iOS Dev", + "tvOS Distribution Profile: test.bundle.id", + "tvOS Dev", + "Mac Distribution Profile: test.bundle.id", + "Mac Dev", + } + + for _, profileName := range xcodeManagedNames { + require.Equal(t, true, IsXcodeManaged(profileName)) + } + + for _, profileName := range nonXcodeManagedNames { + require.Equal(t, false, IsXcodeManaged(profileName)) + } +} + +func TestProvisioningProfilePlatform(t *testing.T) { + tests := []struct { + name string + profileContent string + want ProfileType + }{ + { + name: "iOS", + profileContent: iosProfileContent, + want: ProfileTypeIos, + }, + { + name: "macOS", + profileContent: macosProfileContent, + want: ProfileTypeMacOs, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + profilePkcs7 := pkcs7.PKCS7{Content: []byte(tt.profileContent)} + got, err := NewProvisioningProfileInfo(profilePkcs7) + + require.NoError(t, err) + require.Equal(t, tt.want, got.Type) + }) + } +} + +const iosProfileContent = ` + + + + AppIDName + Bitrise Test + ApplicationIdentifierPrefix + + 9NS44DLTN7 + + CreationDate + 2016-09-22T11:28:46Z + Platform + + iOS + + DeveloperCertificates + + + + Entitlements + + keychain-access-groups + + 9NS44DLTN7.* + + get-task-allow + + application-identifier + 9NS44DLTN7.* + com.apple.developer.team-identifier + 9NS44DLTN7 + + ExpirationDate + 2017-09-22T11:28:46Z + Name + Bitrise Test Development + ProvisionedDevices + + b13813075ad9b298cb9a9f28555c49573d8bc322 + + TeamIdentifier + + 9NS44DLTN7 + + TeamName + Some Dude + TimeToLive + 365 + UUID + 4b617a5f-e31e-4edc-9460-718a5abacd05 + Version + 1 +` + +const macosProfileContent = ` + + + + AppIDName + XC io bitrise mobile ios QuickActionsTodayExtension + ApplicationIdentifierPrefix + + 72SA8V3WYL + + CreationDate + 2022-02-28T10:35:39Z + Platform + + OSX + + IsXcodeManaged + + DeveloperCertificates + + + + + DER-Encoded-Profile + + + Entitlements + + + com.apple.developer.game-center + + + com.apple.security.application-groups + + group.io.bitrise.statistics + + + application-identifier + 72SA8V3WYL.io.bitrise.mobile.ios.QuickActionsTodayExtension + + com.apple.application-identifier + 72SA8V3WYL.io.bitrise.mobile.ios.QuickActionsTodayExtension + + keychain-access-groups + + 72SA8V3WYL.* + com.apple.token + + + get-task-allow + + + com.apple.developer.team-identifier + 72SA8V3WYL + + + ExpirationDate + 2023-02-28T10:35:39Z + Name + _profile_bug_type_catalyst + ProvisionedDevices + + BA0EC799-F254-5574-B335-E70B8A2FA5E7 + + TeamIdentifier + + 72SA8V3WYL + + TeamName + BITFALL FEJLESZTO KORLATOLT FELELOSSEGU TARSASAG + TimeToLive + 365 + UUID + dea6a48c-d7d3-4624-9f6b-e0c3b3ce517d + Version + 1 + +` diff --git a/profileutil/plist_data.go b/profileutil/plist_data.go new file mode 100644 index 00000000..de9dafe1 --- /dev/null +++ b/profileutil/plist_data.go @@ -0,0 +1,171 @@ +package profileutil + +import ( + "strings" + "time" + + "github.com/bitrise-io/go-xcode/exportoptions" + "github.com/bitrise-io/go-xcode/plistutil" + "howett.net/plist" +) + +// PlistData ... +type PlistData plistutil.PlistData + +// NewPlistDataFromFile ... +func NewPlistDataFromFile(provisioningProfilePth string) (PlistData, error) { + provisioningProfilePKCS7, err := ProvisioningProfileFromFile(provisioningProfilePth) + if err != nil { + return PlistData{}, err + } + + var plistData plistutil.PlistData + if _, err := plist.Unmarshal(provisioningProfilePKCS7.Content, &plistData); err != nil { + return PlistData{}, err + } + + return PlistData(plistData), nil +} + +// GetUUID ... +func (profile PlistData) GetUUID() string { + data := plistutil.PlistData(profile) + uuid, _ := data.GetString("UUID") + return uuid +} + +// GetName ... +func (profile PlistData) GetName() string { + data := plistutil.PlistData(profile) + uuid, _ := data.GetString("Name") + return uuid +} + +// GetApplicationIdentifier ... +func (profile PlistData) GetApplicationIdentifier() string { + data := plistutil.PlistData(profile) + entitlements, ok := data.GetMapStringInterface("Entitlements") + if !ok { + return "" + } + + applicationID, ok := entitlements.GetString("application-identifier") + if !ok { + applicationID, ok = entitlements.GetString("com.apple.application-identifier") + if !ok { + return "" + } + } + return applicationID +} + +// GetBundleIdentifier ... +func (profile PlistData) GetBundleIdentifier() string { + applicationID := profile.GetApplicationIdentifier() + + plistData := plistutil.PlistData(profile) + prefixes, found := plistData.GetStringArray("ApplicationIdentifierPrefix") + if found { + for _, prefix := range prefixes { + applicationID = strings.TrimPrefix(applicationID, prefix+".") + } + } + + teamID := profile.GetTeamID() + return strings.TrimPrefix(applicationID, teamID+".") +} + +// GetExportMethod ... +func (profile PlistData) GetExportMethod() exportoptions.Method { + data := plistutil.PlistData(profile) + entitlements, _ := data.GetMapStringInterface("Entitlements") + platform, _ := data.GetStringArray("Platform") + + if len(platform) != 0 { + switch strings.ToLower(platform[0]) { + case "osx": + _, ok := data.GetStringArray("ProvisionedDevices") + if !ok { + if allDevices, ok := data.GetBool("ProvisionsAllDevices"); ok && allDevices { + return exportoptions.MethodDeveloperID + } + return exportoptions.MethodAppStore + } + return exportoptions.MethodDevelopment + case "ios", "tvos": + _, ok := data.GetStringArray("ProvisionedDevices") + if !ok { + if allDevices, ok := data.GetBool("ProvisionsAllDevices"); ok && allDevices { + return exportoptions.MethodEnterprise + } + return exportoptions.MethodAppStore + } + if allow, ok := entitlements.GetBool("get-task-allow"); ok && allow { + return exportoptions.MethodDevelopment + } + return exportoptions.MethodAdHoc + } + } + + return exportoptions.MethodDefault +} + +// GetEntitlements ... +func (profile PlistData) GetEntitlements() plistutil.PlistData { + data := plistutil.PlistData(profile) + entitlements, _ := data.GetMapStringInterface("Entitlements") + return entitlements +} + +// GetTeamID ... +func (profile PlistData) GetTeamID() string { + data := plistutil.PlistData(profile) + entitlements, ok := data.GetMapStringInterface("Entitlements") + if ok { + teamID, _ := entitlements.GetString("com.apple.developer.team-identifier") + return teamID + } + return "" +} + +// GetExpirationDate ... +func (profile PlistData) GetExpirationDate() time.Time { + data := plistutil.PlistData(profile) + expiry, _ := data.GetTime("ExpirationDate") + return expiry +} + +// GetProvisionedDevices ... +func (profile PlistData) GetProvisionedDevices() []string { + data := plistutil.PlistData(profile) + devices, _ := data.GetStringArray("ProvisionedDevices") + return devices +} + +// GetDeveloperCertificates ... +func (profile PlistData) GetDeveloperCertificates() [][]byte { + data := plistutil.PlistData(profile) + developerCertificates, _ := data.GetByteArrayArray("DeveloperCertificates") + return developerCertificates +} + +// GetTeamName ... +func (profile PlistData) GetTeamName() string { + data := plistutil.PlistData(profile) + teamName, _ := data.GetString("TeamName") + return teamName +} + +// GetCreationDate ... +func (profile PlistData) GetCreationDate() time.Time { + data := plistutil.PlistData(profile) + creationDate, _ := data.GetTime("CreationDate") + return creationDate +} + +// GetProvisionsAllDevices ... +func (profile PlistData) GetProvisionsAllDevices() bool { + data := plistutil.PlistData(profile) + provisionsAlldevices, _ := data.GetBool("ProvisionsAllDevices") + return provisionsAlldevices +} diff --git a/profileutil/plist_data_test.go b/profileutil/plist_data_test.go new file mode 100644 index 00000000..7a0d08d9 --- /dev/null +++ b/profileutil/plist_data_test.go @@ -0,0 +1,375 @@ +package profileutil + +import ( + "testing" + + "github.com/bitrise-io/go-xcode/exportoptions" + "github.com/bitrise-io/go-xcode/plistutil" + "github.com/stretchr/testify/require" +) + +func TestPlistData(t *testing.T) { + t.Log("development profile specifies development export method") + { + profile, err := plistutil.NewPlistDataFromContent(developmentProfileContent) + require.NoError(t, err) + require.Equal(t, "4b617a5f-e31e-4edc-9460-718a5abacd05", PlistData(profile).GetUUID()) + require.Equal(t, "Bitrise Test Development", PlistData(profile).GetName()) + require.Equal(t, "9NS44DLTN7.*", PlistData(profile).GetApplicationIdentifier()) + require.Equal(t, "*", PlistData(profile).GetBundleIdentifier()) + require.Equal(t, exportoptions.MethodDevelopment, PlistData(profile).GetExportMethod()) + require.Equal(t, "9NS44DLTN7", PlistData(profile).GetTeamID()) + require.Equal(t, "Some Dude", PlistData(profile).GetTeamName()) + require.Equal(t, "2016-09-22T11:28:46Z", PlistData(profile).GetCreationDate().Format("2006-01-02T15:04:05Z")) + require.Equal(t, "2017-09-22T11:28:46Z", PlistData(profile).GetExpirationDate().Format("2006-01-02T15:04:05Z")) + require.Equal(t, []string{"b13813075ad9b298cb9a9f28555c49573d8bc322"}, PlistData(profile).GetProvisionedDevices()) + require.Equal(t, [][]uint8{[]uint8{}}, PlistData(profile).GetDeveloperCertificates()) + require.Equal(t, false, PlistData(profile).GetProvisionsAllDevices()) + } + + t.Log("app store profile specifies app-store export method") + { + profile, err := plistutil.NewPlistDataFromContent(appStoreProfileContent) + require.NoError(t, err) + require.Equal(t, "a60668dd-191a-4770-8b1e-b453b87aa60b", PlistData(profile).GetUUID()) + require.Equal(t, "Bitrise Test App Store", PlistData(profile).GetName()) + require.Equal(t, "9NS44DLTN7.*", PlistData(profile).GetApplicationIdentifier()) + require.Equal(t, "*", PlistData(profile).GetBundleIdentifier()) + require.Equal(t, exportoptions.MethodAppStore, PlistData(profile).GetExportMethod()) + require.Equal(t, "9NS44DLTN7", PlistData(profile).GetTeamID()) + require.Equal(t, "Some Dude", PlistData(profile).GetTeamName()) + require.Equal(t, "2016-09-22T11:29:12Z", PlistData(profile).GetCreationDate().Format("2006-01-02T15:04:05Z")) + require.Equal(t, "2017-09-21T13:20:06Z", PlistData(profile).GetExpirationDate().Format("2006-01-02T15:04:05Z")) + require.Equal(t, []string(nil), PlistData(profile).GetProvisionedDevices()) + require.Equal(t, [][]uint8{[]uint8{}}, PlistData(profile).GetDeveloperCertificates()) + require.Equal(t, false, PlistData(profile).GetProvisionsAllDevices()) + } + + t.Log("ad hoc profile specifies ad-hoc export method") + { + profile, err := plistutil.NewPlistDataFromContent(adHocProfileContent) + require.NoError(t, err) + require.Equal(t, "26668300-5743-46a1-8e00-7023e2e35c7d", PlistData(profile).GetUUID()) + require.Equal(t, "Bitrise Test Ad Hoc", PlistData(profile).GetName()) + require.Equal(t, "9NS44DLTN7.*", PlistData(profile).GetApplicationIdentifier()) + require.Equal(t, "*", PlistData(profile).GetBundleIdentifier()) + require.Equal(t, exportoptions.MethodAdHoc, PlistData(profile).GetExportMethod()) + require.Equal(t, "9NS44DLTN7", PlistData(profile).GetTeamID()) + require.Equal(t, "Some Dude", PlistData(profile).GetTeamName()) + require.Equal(t, "2016-09-22T11:29:38Z", PlistData(profile).GetCreationDate().Format("2006-01-02T15:04:05Z")) + require.Equal(t, "2017-09-21T13:20:06Z", PlistData(profile).GetExpirationDate().Format("2006-01-02T15:04:05Z")) + require.Equal(t, []string{"b13813075ad9b298cb9a9f28555c49573d8bc322"}, PlistData(profile).GetProvisionedDevices()) + require.Equal(t, [][]uint8{[]uint8{}}, PlistData(profile).GetDeveloperCertificates()) + require.Equal(t, false, PlistData(profile).GetProvisionsAllDevices()) + } + + t.Log("it creates model from enterprise profile content") + { + profile, err := plistutil.NewPlistDataFromContent(enterpriseProfileContent) + require.NoError(t, err) + require.Equal(t, "8d6caa15-ac49-48f9-9bd3-ce9244add6a0", PlistData(profile).GetUUID()) + require.Equal(t, "Bitrise Test Enterprise", PlistData(profile).GetName()) + require.Equal(t, "9NS44DLTN7.com.Bitrise.Test", PlistData(profile).GetApplicationIdentifier()) + require.Equal(t, "com.Bitrise.Test", PlistData(profile).GetBundleIdentifier()) + require.Equal(t, exportoptions.MethodEnterprise, PlistData(profile).GetExportMethod()) + require.Equal(t, "9NS44DLTN7", PlistData(profile).GetTeamID()) + require.Equal(t, "Bitrise", PlistData(profile).GetTeamName()) + require.Equal(t, "2015-10-05T13:32:46Z", PlistData(profile).GetCreationDate().Format("2006-01-02T15:04:05Z")) + require.Equal(t, "2016-10-04T13:32:46Z", PlistData(profile).GetExpirationDate().Format("2006-01-02T15:04:05Z")) + require.Equal(t, []string(nil), PlistData(profile).GetProvisionedDevices()) + require.Equal(t, [][]uint8{[]uint8{}}, PlistData(profile).GetDeveloperCertificates()) + require.Equal(t, true, PlistData(profile).GetProvisionsAllDevices()) + } +} + +func TestTVOSPlistData(t *testing.T) { + t.Log("it creates model from tvOS appstore profile content") + { + profile, err := plistutil.NewPlistDataFromContent(tvOSAppStoreProfileContent) + require.NoError(t, err) + require.Equal(t, "dec523d5-624b-44bd-8d16-6d1d69c63276", PlistData(profile).GetUUID()) + require.Equal(t, "Bitrise app-store - (bdh.NPO-Live.bitrise.sample)", PlistData(profile).GetName()) + require.Equal(t, "72SA8V3WYL.bdh.NPO-Live.bitrise.sample", PlistData(profile).GetApplicationIdentifier()) + require.Equal(t, "bdh.NPO-Live.bitrise.sample", PlistData(profile).GetBundleIdentifier()) + require.Equal(t, exportoptions.MethodAppStore, PlistData(profile).GetExportMethod()) + require.Equal(t, "72SA8V3WYL", PlistData(profile).GetTeamID()) + require.Equal(t, "Bitrise", PlistData(profile).GetTeamName()) + require.Equal(t, "2018-10-24T11:22:30Z", PlistData(profile).GetCreationDate().Format("2006-01-02T15:04:05Z")) + require.Equal(t, "2019-04-16T08:42:18Z", PlistData(profile).GetExpirationDate().Format("2006-01-02T15:04:05Z")) + require.Equal(t, []string(nil), PlistData(profile).GetProvisionedDevices()) + require.Equal(t, [][]uint8{[]uint8{}}, PlistData(profile).GetDeveloperCertificates()) + require.Equal(t, false, PlistData(profile).GetProvisionsAllDevices()) + } +} + +const developmentProfileContent = ` + + + + AppIDName + Bitrise Test + ApplicationIdentifierPrefix + + 9NS44DLTN7 + + CreationDate + 2016-09-22T11:28:46Z + Platform + + iOS + + DeveloperCertificates + + + + Entitlements + + keychain-access-groups + + 9NS44DLTN7.* + + get-task-allow + + application-identifier + 9NS44DLTN7.* + com.apple.developer.team-identifier + 9NS44DLTN7 + + ExpirationDate + 2017-09-22T11:28:46Z + Name + Bitrise Test Development + ProvisionedDevices + + b13813075ad9b298cb9a9f28555c49573d8bc322 + + TeamIdentifier + + 9NS44DLTN7 + + TeamName + Some Dude + TimeToLive + 365 + UUID + 4b617a5f-e31e-4edc-9460-718a5abacd05 + Version + 1 +` + +const appStoreProfileContent = ` + + + + AppIDName + Bitrise Test + ApplicationIdentifierPrefix + + 9NS44DLTN7 + + CreationDate + 2016-09-22T11:29:12Z + Platform + + iOS + + DeveloperCertificates + + + + Entitlements + + keychain-access-groups + + 9NS44DLTN7.* + + get-task-allow + + application-identifier + 9NS44DLTN7.* + com.apple.developer.team-identifier + 9NS44DLTN7 + beta-reports-active + + + ExpirationDate + 2017-09-21T13:20:06Z + Name + Bitrise Test App Store + TeamIdentifier + + 9NS44DLTN7 + + TeamName + Some Dude + TimeToLive + 364 + UUID + a60668dd-191a-4770-8b1e-b453b87aa60b + Version + 1 +` + +const adHocProfileContent = ` + + + + AppIDName + Bitrise Test + ApplicationIdentifierPrefix + + 9NS44DLTN7 + + CreationDate + 2016-09-22T11:29:38Z + Platform + + iOS + + DeveloperCertificates + + + + Entitlements + + keychain-access-groups + + 9NS44DLTN7.* + + get-task-allow + + application-identifier + 9NS44DLTN7.* + com.apple.developer.team-identifier + 9NS44DLTN7 + + ExpirationDate + 2017-09-21T13:20:06Z + Name + Bitrise Test Ad Hoc + ProvisionedDevices + + b13813075ad9b298cb9a9f28555c49573d8bc322 + + TeamIdentifier + + 9NS44DLTN7 + + TeamName + Some Dude + TimeToLive + 364 + UUID + 26668300-5743-46a1-8e00-7023e2e35c7d + Version + 1 +` + +const enterpriseProfileContent = ` + + + + AppIDName + Test + ApplicationIdentifierPrefix + + 9NS44DLTN7 + + CreationDate + 2015-10-05T13:32:46Z + Platform + + iOS + + DeveloperCertificates + + + + Entitlements + + keychain-access-groups + + 9NS44DLTN7.* + + get-task-allow + + application-identifier + 9NS44DLTN7.com.Bitrise.Test + com.apple.developer.team-identifier + 9NS44DLTN7 + + + ExpirationDate + 2016-10-04T13:32:46Z + Name + Bitrise Test Enterprise + ProvisionsAllDevices + + TeamIdentifier + + 9NS44DLTN7 + + TeamName + Bitrise + TimeToLive + 365 + UUID + 8d6caa15-ac49-48f9-9bd3-ce9244add6a0 + Version + 1 +` + +const tvOSAppStoreProfileContent = ` + + + + AppIDName + Bitrise bdh NPOLive bitrise sample be2b4e3cfb0f2a967b404820aa18e09c + ApplicationIdentifierPrefix + + 72SA8V3WYL + + CreationDate + 2018-10-24T11:22:30Z + Platform + + tvOS + + DeveloperCertificates + + + + IsXcodeManaged + + Entitlements + + keychain-access-groups + + 72SA8V3WYL.* + + get-task-allow + + application-identifier + 72SA8V3WYL.bdh.NPO-Live.bitrise.sample + com.apple.developer.team-identifier + 72SA8V3WYL + beta-reports-active + + + ExpirationDate + 2019-04-16T08:42:18Z + Name + Bitrise app-store - (bdh.NPO-Live.bitrise.sample) + TeamIdentifier + + 72SA8V3WYL + + TeamName + Bitrise + TimeToLive + 173 + UUID + dec523d5-624b-44bd-8d16-6d1d69c63276 + Version + 1 +` diff --git a/profileutil/util.go b/profileutil/util.go new file mode 100644 index 00000000..015d4780 --- /dev/null +++ b/profileutil/util.go @@ -0,0 +1,110 @@ +package profileutil + +import ( + "path/filepath" + + "github.com/bitrise-io/go-utils/fileutil" + "github.com/bitrise-io/go-utils/pathutil" + "github.com/fullsailor/pkcs7" +) + +// ProfileType ... +type ProfileType string + +// ProfileTypeIos ... +const ProfileTypeIos ProfileType = "ios" + +// ProfileTypeMacOs ... +const ProfileTypeMacOs ProfileType = "osx" + +// ProfileTypeTvOs ... +const ProfileTypeTvOs ProfileType = "tvos" + +// ProvProfileSystemDirPath ... +const ProvProfileSystemDirPath = "~/Library/MobileDevice/Provisioning Profiles" + +// ProvisioningProfileFromContent ... +func ProvisioningProfileFromContent(content []byte) (*pkcs7.PKCS7, error) { + return pkcs7.Parse(content) +} + +// ProvisioningProfileFromFile ... +func ProvisioningProfileFromFile(pth string) (*pkcs7.PKCS7, error) { + content, err := fileutil.ReadBytesFromFile(pth) + if err != nil { + return nil, err + } + return ProvisioningProfileFromContent(content) +} + +// InstalledProvisioningProfiles ... +func InstalledProvisioningProfiles(profileType ProfileType) ([]*pkcs7.PKCS7, error) { + ext := ".mobileprovision" + if profileType == ProfileTypeMacOs { + ext = ".provisionprofile" + } + + absProvProfileDirPath, err := pathutil.AbsPath(ProvProfileSystemDirPath) + if err != nil { + return nil, err + } + + pattern := filepath.Join(pathutil.EscapeGlobPath(absProvProfileDirPath), "*"+ext) + pths, err := filepath.Glob(pattern) + if err != nil { + return nil, err + } + + profiles := []*pkcs7.PKCS7{} + for _, pth := range pths { + profile, err := ProvisioningProfileFromFile(pth) + if err != nil { + return nil, err + } + profiles = append(profiles, profile) + } + return profiles, nil +} + +// FindProvisioningProfile ... +func FindProvisioningProfile(uuid string) (*pkcs7.PKCS7, string, error) { + { + iosProvisioningProfileExt := ".mobileprovision" + absProvProfileDirPath, err := pathutil.AbsPath(ProvProfileSystemDirPath) + if err != nil { + return nil, "", err + } + + pth := filepath.Join(absProvProfileDirPath, uuid+iosProvisioningProfileExt) + if exist, err := pathutil.IsPathExists(pth); err != nil { + return nil, "", err + } else if exist { + profile, err := ProvisioningProfileFromFile(pth) + if err != nil { + return nil, "", err + } + return profile, pth, nil + } + } + + { + macOsProvisioningProfileExt := ".provisionprofile" + absProvProfileDirPath, err := pathutil.AbsPath(ProvProfileSystemDirPath) + if err != nil { + return nil, "", err + } + + pth := filepath.Join(absProvProfileDirPath, uuid+macOsProvisioningProfileExt) + if exist, err := pathutil.IsPathExists(pth); err != nil { + return nil, "", err + } else if exist { + profile, err := ProvisioningProfileFromFile(pth) + if err != nil { + return nil, "", err + } + return profile, pth, nil + } + } + + return nil, "", nil +} diff --git a/xcarchive/ios_test.go b/xcarchive/ios_test.go index cbd28bcd..c680aafa 100644 --- a/xcarchive/ios_test.go +++ b/xcarchive/ios_test.go @@ -4,8 +4,8 @@ import ( "reflect" "testing" - "github.com/bitrise-io/go-xcode/profileutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" + "github.com/bitrise-io/go-xcode/v2/profileutil" v1xcarchive "github.com/bitrise-io/go-xcode/xcarchive" ) From f9903ec2807a66dc05fa5cc2f331998684f8e4f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krisztia=CC=81n=20Go=CC=88drei?= Date: Fri, 20 Jun 2025 13:50:30 +0200 Subject: [PATCH 3/7] Move xcarchive to v2 --- .../archive_info_provider.go | 2 +- profileutil/info_model.go | 3 +- xcarchive/entitlements.go | 46 +++ xcarchive/entitlements_test.go | 74 ++++ xcarchive/ios.go | 370 ++++++++++++++++- xcarchive/ios_test.go | 390 ++++++++++++++---- xcarchive/macos.go | 231 +++++++++++ xcarchive/macos_test.go | 78 ++++ xcarchive/xcarchive.go | 88 ++++ xcarchive/xcarchive_test.go | 166 ++++++++ 10 files changed, 1366 insertions(+), 82 deletions(-) create mode 100644 xcarchive/entitlements.go create mode 100644 xcarchive/entitlements_test.go create mode 100644 xcarchive/macos.go create mode 100644 xcarchive/macos_test.go create mode 100644 xcarchive/xcarchive.go create mode 100644 xcarchive/xcarchive_test.go diff --git a/exportoptionsgenerator/archive_info_provider.go b/exportoptionsgenerator/archive_info_provider.go index 54b73f1f..416f2498 100644 --- a/exportoptionsgenerator/archive_info_provider.go +++ b/exportoptionsgenerator/archive_info_provider.go @@ -1,7 +1,7 @@ package exportoptionsgenerator import ( - "github.com/bitrise-io/go-xcode/xcarchive" + "github.com/bitrise-io/go-xcode/v2/xcarchive" ) // ExportProduct ... diff --git a/profileutil/info_model.go b/profileutil/info_model.go index 36c87230..cc1e1075 100644 --- a/profileutil/info_model.go +++ b/profileutil/info_model.go @@ -8,12 +8,13 @@ import ( "strings" "time" + "howett.net/plist" + "github.com/bitrise-io/go-utils/log" "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/fullsailor/pkcs7" - "howett.net/plist" ) // ProvisioningProfileInfoModel ... diff --git a/xcarchive/entitlements.go b/xcarchive/entitlements.go new file mode 100644 index 00000000..7908e5cc --- /dev/null +++ b/xcarchive/entitlements.go @@ -0,0 +1,46 @@ +package xcarchive + +import ( + "fmt" + "path/filepath" + + "github.com/bitrise-io/go-utils/command" + "github.com/bitrise-io/go-xcode/plistutil" +) + +func executableNameFromInfoPlist(infoPlist plistutil.PlistData) string { + if name, ok := infoPlist.GetString("CFBundleExecutable"); ok { + return name + } + return "" +} + +func getEntitlements(basePath, executableRelativePath string) (plistutil.PlistData, error) { + entitlements, err := entitlementsFromExecutable(basePath, executableRelativePath) + if err != nil { + return plistutil.PlistData{}, err + } + + if entitlements != nil { + return *entitlements, nil + } + + return plistutil.PlistData{}, nil +} + +func entitlementsFromExecutable(basePath, executableRelativePath string) (*plistutil.PlistData, error) { + fmt.Printf("Fetching entitlements from executable") + + cmd := command.New("codesign", "--display", "--entitlements", ":-", filepath.Join(basePath, executableRelativePath)) + entitlementsString, err := cmd.RunAndReturnTrimmedOutput() + if err != nil { + return nil, err + } + + plist, err := plistutil.NewPlistDataFromContent(entitlementsString) + if err != nil { + return nil, err + } + + return &plist, nil +} diff --git a/xcarchive/entitlements_test.go b/xcarchive/entitlements_test.go new file mode 100644 index 00000000..e6de76a2 --- /dev/null +++ b/xcarchive/entitlements_test.go @@ -0,0 +1,74 @@ +package xcarchive + +import ( + "path/filepath" + "testing" + + "github.com/bitrise-io/go-utils/pathutil" + + "github.com/bitrise-io/go-xcode/plistutil" + "github.com/stretchr/testify/assert" +) + +func TestGiveniOS_WhenAskingForEntitlements_ThenReadsItFromTheExecutable(t *testing.T) { + // Given + appPath := filepath.Join(sampleRepoPath(t), "archives/Fruta.xcarchive/Products/Applications/Fruta.app") + executable := executableRelativePath(appPath, "Info.plist", "") + + // When + entitlements, err := getEntitlements(appPath, executable) + + // Then + assert.NoError(t, err) + assert.Equal(t, iosEntitlements(), entitlements) +} + +func TestGivenMacos_WhenAskingForEntitlements_ThenReadsItFromTheExecutable(t *testing.T) { + // Given + appPath := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive/Products/Applications/Test.app") + executable := executableRelativePath(appPath, "Contents/Info.plist", "Contents/MacOS/") + + // When + entitlements, err := getEntitlements(appPath, executable) + + // Then + assert.NoError(t, err) + assert.Equal(t, macosEntitlements(), entitlements) +} + +func executableRelativePath(basePath, infoPlistRelativePath, executableFolderRelativePath string) string { + infoPlistPath := filepath.Join(basePath, infoPlistRelativePath) + exist, err := pathutil.IsPathExists(infoPlistPath) + if err != nil { + return "" + } + + if exist == false { + return "" + } + + plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + if err != nil { + return "" + } + + return filepath.Join(executableFolderRelativePath, executableNameFromInfoPlist(plist)) +} + +func iosEntitlements() plistutil.PlistData { + return map[string]interface{}{ + "application-identifier": "72SA8V3WYL.io.bitrise.appcliptest", + "com.apple.developer.applesignin": []interface{}{"Default"}, + "com.apple.developer.icloud-container-identifiers": []interface{}{}, + "com.apple.developer.team-identifier": "72SA8V3WYL", + "com.apple.security.application-groups": []interface{}{"group.io.bitrise.appcliptest"}, + "get-task-allow": false, + } +} + +func macosEntitlements() plistutil.PlistData { + return map[string]interface{}{ + "com.apple.security.app-sandbox": true, + "com.apple.security.files.user-selected.read-only": true, + } +} diff --git a/xcarchive/ios.go b/xcarchive/ios.go index b854c3c3..61e7f511 100644 --- a/xcarchive/ios.go +++ b/xcarchive/ios.go @@ -1,29 +1,387 @@ package xcarchive import ( + "errors" "fmt" + "path/filepath" + "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" - "github.com/bitrise-io/go-xcode/xcarchive" + "github.com/bitrise-io/go-xcode/v2/profileutil" ) +// IosBaseApplication ... +type IosBaseApplication struct { + Path string + InfoPlist plistutil.PlistData + Entitlements plistutil.PlistData + ProvisioningProfile profileutil.ProvisioningProfileInfoModel +} + +// BundleIdentifier ... +func (app IosBaseApplication) BundleIdentifier() string { + bundleID, _ := app.InfoPlist.GetString("CFBundleIdentifier") + return bundleID +} + +// NewIosBaseApplication ... +func NewIosBaseApplication(path string) (IosBaseApplication, error) { + var infoPlist plistutil.PlistData + { + infoPlistPath := filepath.Join(path, "Info.plist") + if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { + return IosBaseApplication{}, fmt.Errorf("failed to check if Info.plist exists at: %s, error: %s", infoPlistPath, err) + } else if !exist { + return IosBaseApplication{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) + } + plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + if err != nil { + return IosBaseApplication{}, err + } + infoPlist = plist + } + + var provisioningProfile profileutil.ProvisioningProfileInfoModel + { + provisioningProfilePath := filepath.Join(path, "embedded.mobileprovision") + if exist, err := pathutil.IsPathExists(provisioningProfilePath); err != nil { + return IosBaseApplication{}, fmt.Errorf("failed to check if profile exists at: %s, error: %s", provisioningProfilePath, err) + } else if !exist { + return IosBaseApplication{}, fmt.Errorf("profile not exists at: %s", provisioningProfilePath) + } + + profile, err := profileutil.NewProvisioningProfileInfoFromFile(provisioningProfilePath) + if err != nil { + return IosBaseApplication{}, err + } + provisioningProfile = profile + } + + executable := executableNameFromInfoPlist(infoPlist) + entitlements, err := getEntitlements(path, executable) + if err != nil { + return IosBaseApplication{}, err + } + + return IosBaseApplication{ + Path: path, + InfoPlist: infoPlist, + Entitlements: entitlements, + ProvisioningProfile: provisioningProfile, + }, nil +} + +// IosExtension ... +type IosExtension struct { + IosBaseApplication +} + +// NewIosExtension ... +func NewIosExtension(path string) (IosExtension, error) { + baseApp, err := NewIosBaseApplication(path) + if err != nil { + return IosExtension{}, err + } + + return IosExtension{ + baseApp, + }, nil +} + +// IosWatchApplication ... +type IosWatchApplication struct { + IosBaseApplication + Extensions []IosExtension +} + +// IosClipApplication ... +type IosClipApplication struct { + IosBaseApplication +} + +// NewIosWatchApplication ... +func NewIosWatchApplication(path string) (IosWatchApplication, error) { + baseApp, err := NewIosBaseApplication(path) + if err != nil { + return IosWatchApplication{}, err + } + + extensions := []IosExtension{} + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "PlugIns/*.appex") + pths, err := filepath.Glob(pattern) + if err != nil { + return IosWatchApplication{}, fmt.Errorf("failed to search for watch application's extensions using pattern: %s, error: %s", pattern, err) + } + for _, pth := range pths { + extension, err := NewIosExtension(pth) + if err != nil { + return IosWatchApplication{}, err + } + + extensions = append(extensions, extension) + } + + return IosWatchApplication{ + IosBaseApplication: baseApp, + Extensions: extensions, + }, nil +} + +// NewIosClipApplication ... +func NewIosClipApplication(path string) (IosClipApplication, error) { + baseApp, err := NewIosBaseApplication(path) + if err != nil { + return IosClipApplication{}, err + } + + return IosClipApplication{ + IosBaseApplication: baseApp, + }, nil +} + +// IosApplication ... +type IosApplication struct { + IosBaseApplication + WatchApplication *IosWatchApplication + ClipApplication *IosClipApplication + Extensions []IosExtension +} + +// NewIosApplication ... +func NewIosApplication(path string) (IosApplication, error) { + baseApp, err := NewIosBaseApplication(path) + if err != nil { + return IosApplication{}, err + } + + var watchApp *IosWatchApplication + { + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "Watch/*.app") + pths, err := filepath.Glob(pattern) + if err != nil { + return IosApplication{}, err + } + if len(pths) > 0 { + watchPath := pths[0] + app, err := NewIosWatchApplication(watchPath) + if err != nil { + return IosApplication{}, err + } + watchApp = &app + } + } + + var clipApp *IosClipApplication + { + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "AppClips/*.app") + pths, err := filepath.Glob(pattern) + if err != nil { + return IosApplication{}, err + } + if len(pths) > 0 { + clipPath := pths[0] + app, err := NewIosClipApplication(clipPath) + if err != nil { + return IosApplication{}, err + } + clipApp = &app + } + } + + extensions := []IosExtension{} + { + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "PlugIns/*.appex") + pths, err := filepath.Glob(pattern) + if err != nil { + return IosApplication{}, fmt.Errorf("failed to search for watch application's extensions using pattern: %s, error: %s", pattern, err) + } + for _, pth := range pths { + extension, err := NewIosExtension(pth) + if err != nil { + return IosApplication{}, err + } + + extensions = append(extensions, extension) + } + } + + return IosApplication{ + IosBaseApplication: baseApp, + WatchApplication: watchApp, + ClipApplication: clipApp, + Extensions: extensions, + }, nil +} + // IosArchive ... type IosArchive struct { - xcarchive.IosArchive + Path string + InfoPlist plistutil.PlistData + Application IosApplication } // NewIosArchive ... func NewIosArchive(path string) (IosArchive, error) { - archive, err := xcarchive.NewIosArchive(path) + var infoPlist plistutil.PlistData + { + infoPlistPath := filepath.Join(path, "Info.plist") + if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { + return IosArchive{}, fmt.Errorf("failed to check if Info.plist exists at: %s, error: %s", infoPlistPath, err) + } else if !exist { + return IosArchive{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) + } + plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + if err != nil { + return IosArchive{}, err + } + infoPlist = plist + } + + var application IosApplication + { + appPath := "" + if appRelativePathToProducts, found := applicationFromPlist(infoPlist); found { + appPath = filepath.Join(path, "Products", appRelativePathToProducts) + } else { + var err error + if appPath, err = applicationFromArchive(path); err != nil { + return IosArchive{}, err + } + } + if exist, err := pathutil.IsPathExists(appPath); err != nil { + return IosArchive{}, fmt.Errorf("failed to check if app exists, path: %s, error: %s", appPath, err) + } else if !exist { + return IosArchive{}, fmt.Errorf("application not found on path: %s, error: %s", appPath, err) + } + + app, err := NewIosApplication(appPath) + if err != nil { + return IosArchive{}, err + } + application = app + } return IosArchive{ - IosArchive: archive, - }, err + Path: path, + InfoPlist: infoPlist, + Application: application, + }, nil +} + +func applicationFromPlist(InfoPlist plistutil.PlistData) (string, bool) { + if properties, found := InfoPlist.GetMapStringInterface("ApplicationProperties"); found { + return properties.GetString("ApplicationPath") + } + return "", false +} + +func applicationFromArchive(path string) (string, error) { + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "Products/Applications/*.app") + pths, err := filepath.Glob(pattern) + if err != nil { + return "", err + } + if len(pths) == 0 { + return "", fmt.Errorf("failed to find main app, using pattern: %s", pattern) + } + return pths[0], nil } // IsSigningManagedAutomatically ... func (archive IosArchive) IsSigningManagedAutomatically() (bool, error) { - return archive.IsXcodeManaged(), nil + return archive.Application.ProvisioningProfile.IsXcodeManaged(), nil +} + +// SigningIdentity ... +func (archive IosArchive) SigningIdentity() string { + if properties, found := archive.InfoPlist.GetMapStringInterface("ApplicationProperties"); found { + identity, _ := properties.GetString("SigningIdentity") + return identity + } + return "" +} + +// BundleIDEntitlementsMap ... +func (archive IosArchive) BundleIDEntitlementsMap() map[string]plistutil.PlistData { + bundleIDEntitlementsMap := map[string]plistutil.PlistData{} + + bundleID := archive.Application.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = archive.Application.Entitlements + + for _, plugin := range archive.Application.Extensions { + bundleID := plugin.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = plugin.Entitlements + } + + if archive.Application.WatchApplication != nil { + watchApplication := *archive.Application.WatchApplication + + bundleID := watchApplication.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = watchApplication.Entitlements + + for _, plugin := range watchApplication.Extensions { + bundleID := plugin.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = plugin.Entitlements + } + } + + if archive.Application.ClipApplication != nil { + clipApplication := *archive.Application.ClipApplication + + bundleID := clipApplication.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = clipApplication.Entitlements + } + + return bundleIDEntitlementsMap +} + +// BundleIDProfileInfoMap ... +func (archive IosArchive) BundleIDProfileInfoMap() map[string]profileutil.ProvisioningProfileInfoModel { + bundleIDProfileMap := map[string]profileutil.ProvisioningProfileInfoModel{} + + bundleID := archive.Application.BundleIdentifier() + bundleIDProfileMap[bundleID] = archive.Application.ProvisioningProfile + + for _, plugin := range archive.Application.Extensions { + bundleID := plugin.BundleIdentifier() + bundleIDProfileMap[bundleID] = plugin.ProvisioningProfile + } + + if archive.Application.WatchApplication != nil { + watchApplication := *archive.Application.WatchApplication + + bundleID := watchApplication.BundleIdentifier() + bundleIDProfileMap[bundleID] = watchApplication.ProvisioningProfile + + for _, plugin := range watchApplication.Extensions { + bundleID := plugin.BundleIdentifier() + bundleIDProfileMap[bundleID] = plugin.ProvisioningProfile + } + } + + if archive.Application.ClipApplication != nil { + clipApplication := *archive.Application.ClipApplication + + bundleID := clipApplication.BundleIdentifier() + bundleIDProfileMap[bundleID] = clipApplication.ProvisioningProfile + } + + return bundleIDProfileMap +} + +// FindDSYMs ... +func (archive IosArchive) FindDSYMs() ([]string, []string, error) { + return findDSYMs(archive.Path) +} + +// TeamID ... +func (archive IosArchive) TeamID() (string, error) { + bundleIDProfileInfoMap := archive.BundleIDProfileInfoMap() + for _, profileInfo := range bundleIDProfileInfoMap { + return profileInfo.TeamID, nil + } + return "", errors.New("team id not found") } // Platform ... diff --git a/xcarchive/ios_test.go b/xcarchive/ios_test.go index c680aafa..657e4eff 100644 --- a/xcarchive/ios_test.go +++ b/xcarchive/ios_test.go @@ -1,15 +1,263 @@ package xcarchive import ( + "fmt" + "os" + "path/filepath" "reflect" "testing" + "github.com/bitrise-io/go-utils/command" + "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/profileutil" - v1xcarchive "github.com/bitrise-io/go-xcode/xcarchive" + "github.com/stretchr/testify/require" ) -func TestIosArchive_GetAppLayout(t *testing.T) { +var tmpDir = "" + +func sampleRepoPath(t *testing.T) string { + dir := "" + if tmpDir != "" { + dir = tmpDir + } else { + var err error + dir, err = pathutil.NormalizedOSTempDirPath(tempDirName) + require.NoError(t, err) + sampleArtifactsGitURI := "https://github.com/bitrise-io/sample-artifacts.git" + cmd := command.New("git", "clone", sampleArtifactsGitURI, dir) + output, err := cmd.RunAndReturnTrimmedCombinedOutput() + if err != nil { + t.Log(output) + t.Errorf("git clone failed: %s", err) + } + tmpDir = dir + } + t.Logf("sample artifcats dir: %s\n", dir) + return dir +} + +func TestNewIosArchive(t *testing.T) { + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + require.Equal(t, 5, len(archive.InfoPlist)) + + app := archive.Application + require.Equal(t, 26, len(app.InfoPlist)) + require.Equal(t, 4, len(app.Entitlements)) + require.Equal(t, "*", app.ProvisioningProfile.BundleID) + + require.Equal(t, 1, len(app.Extensions)) + extension := app.Extensions[0] + require.Equal(t, 23, len(extension.InfoPlist)) + require.Equal(t, 4, len(extension.Entitlements)) + require.Equal(t, "*", extension.ProvisioningProfile.BundleID) + + require.NotNil(t, app.WatchApplication) + watchApp := *app.WatchApplication + require.Equal(t, 24, len(watchApp.InfoPlist)) + require.Equal(t, 4, len(watchApp.Entitlements)) + require.Equal(t, "*", watchApp.ProvisioningProfile.BundleID) + + require.Equal(t, 1, len(watchApp.Extensions)) + watchExtension := watchApp.Extensions[0] + require.Equal(t, 23, len(watchExtension.InfoPlist)) + require.Equal(t, 4, len(watchExtension.Entitlements)) + require.Equal(t, "*", watchExtension.ProvisioningProfile.BundleID) +} + +func TestNewAppClipArchive(t *testing.T) { + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/Fruta.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + require.Equal(t, 5, len(archive.InfoPlist)) + + app := archive.Application + require.Equal(t, 30, len(app.InfoPlist)) + require.Equal(t, 6, len(app.Entitlements)) + require.Equal(t, "io.bitrise.appcliptest", app.ProvisioningProfile.BundleID) + + require.Equal(t, 1, len(app.Extensions)) + extension := app.Extensions[0] + require.Equal(t, 24, len(extension.InfoPlist)) + require.Equal(t, 4, len(extension.Entitlements)) + require.Equal(t, "io.bitrise.appcliptest.ios-widgets", extension.ProvisioningProfile.BundleID) + + require.NotNil(t, app.ClipApplication) + clipApp := *app.ClipApplication + require.Equal(t, 31, len(clipApp.InfoPlist)) + require.Equal(t, 8, len(clipApp.Entitlements)) + require.Equal(t, "io.bitrise.appcliptest.Clip", clipApp.ProvisioningProfile.BundleID) +} + +func TestIsXcodeManaged(t *testing.T) { + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + + managed, _ := archive.IsSigningManagedAutomatically() + require.Equal(t, false, managed) +} + +func TestSigningIdentity(t *testing.T) { + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + + require.Equal(t, "iPhone Developer: Bitrise Bot (VV2J4SV8V4)", archive.SigningIdentity()) +} + +func TestBundleIDEntitlementsMap(t *testing.T) { + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + + bundleIDEntitlementsMap := archive.BundleIDEntitlementsMap() + require.Equal(t, 4, len(bundleIDEntitlementsMap)) + + bundleIDs := []string{"com.bitrise.code-sign-test.share-extension", "com.bitrise.code-sign-test.watchkitapp", "com.bitrise.code-sign-test.watchkitapp.watchkitextension", "com.bitrise.code-sign-test"} + for _, bundleID := range bundleIDs { + _, ok := bundleIDEntitlementsMap[bundleID] + require.True(t, ok, fmt.Sprintf("%v", bundleIDEntitlementsMap)) + } +} + +func TestBundleIDProfileInfoMap(t *testing.T) { + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + + bundleIDProfileInfoMap := archive.BundleIDProfileInfoMap() + require.Equal(t, 4, len(bundleIDProfileInfoMap)) + + bundleIDs := []string{"com.bitrise.code-sign-test.share-extension", "com.bitrise.code-sign-test.watchkitapp", "com.bitrise.code-sign-test.watchkitapp.watchkitextension", "com.bitrise.code-sign-test"} + for _, bundleID := range bundleIDs { + _, ok := bundleIDProfileInfoMap[bundleID] + require.True(t, ok, fmt.Sprintf("%v", bundleIDProfileInfoMap)) + } +} + +func TestFindDSYMs(t *testing.T) { + // base case: dsyms for apps and frameworks + iosArchivePth := filepath.Join(sampleRepoPath(t), "archives/Fruta.xcarchive") + archive, err := NewIosArchive(iosArchivePth) + require.NoError(t, err) + + appDsym, otherDsyms, err := archive.FindDSYMs() + require.NoError(t, err) + require.Equal(t, 2, len(appDsym)) + require.Equal(t, 2, len(otherDsyms)) + + // no app dsym case: something has changed since the + // initial implementation of the function under test, + // and is causing dsyms with filenames to be generated + // even when dsym generation is turned off -- we don't care about + // other dsyms in this case, only whether the app dsym + // path is empty + noDSYMArchivePth := filepath.Join(sampleRepoPath(t), "archives/ios.ios-simple-objc.noappdsym.xcarchive") + archive, err = NewIosArchive(noDSYMArchivePth) + require.NoError(t, err) + + appDsym, _, err = archive.FindDSYMs() + require.NoError(t, err) + require.Empty(t, appDsym) +} + +func Test_applicationFromArchive(t *testing.T) { + var err error + tempDir, err := pathutil.NormalizedOSTempDirPath(t.Name()) + if err != nil { + t.Errorf("setup: failed to create temp dir") + } + archivePath := filepath.Join(tempDir, "{}GlobControlChars:a-b[ab]?*", "test.xcarchive") + appDir := filepath.Join(archivePath, "Products", "Applications") + appPath := filepath.Join(appDir, "test.app") + t.Logf("Test app path: %s", appPath) + err = os.MkdirAll(appDir, os.ModePerm) + if err != nil { + t.Errorf("setup: failed to create directory: %s, error: %s", appDir, err) + } + file, err := os.Create(appPath) + if err != nil { + t.Errorf("setup: failed to create test archive: %s, error: %s", appPath, err) + } + if err := file.Close(); err != nil { + t.Errorf("setup: failed to close file, error: %s", err) + } + + type args struct { + path string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "[] glob control characters in path", + args: args{ + path: archivePath, + }, + want: appPath, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := applicationFromArchive(tt.args.path) + if (err != nil) != tt.wantErr { + t.Errorf("applicationFromArchive() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("applicationFromArchive() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_applicationFromPlist(t *testing.T) { + infoPlist, err := plistutil.NewPlistDataFromFile(filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive/Info.plist")) + const appRelativePathToProduct = "Applications/code-sign-test.app" + if err != nil { + t.Errorf("setup: could not read plist, error: %s", infoPlist) + } + + type args struct { + InfoPlist plistutil.PlistData + } + tests := []struct { + name string + args args + want string + want1 bool + }{ + { + name: "normal case", + args: args{ + infoPlist, + }, + want: appRelativePathToProduct, + want1: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := applicationFromPlist(tt.args.InfoPlist) + if got != tt.want { + t.Errorf("applicationFromPlist() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("applicationFromPlist() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func Test_GetAppLayout(t *testing.T) { tests := []struct { name string archive IosArchive @@ -19,16 +267,14 @@ func TestIosArchive_GetAppLayout(t *testing.T) { { name: "Single target app", archive: IosArchive{ - IosArchive: v1xcarchive.IosArchive{ - Application: v1xcarchive.IosApplication{ - IosBaseApplication: v1xcarchive.IosBaseApplication{ - InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.app", - "DTPlatformName": "iphoneos", - }, - ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ - TeamID: "1234ASDF", - }, + Application: IosApplication{ + IosBaseApplication: IosBaseApplication{ + InfoPlist: map[string]interface{}{ + "CFBundleIdentifier": "io.bitrise.app", + "DTPlatformName": "iphoneos", + }, + ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ + TeamID: "1234ASDF", }, }, }, @@ -43,45 +289,56 @@ func TestIosArchive_GetAppLayout(t *testing.T) { { name: "Multi target app", archive: IosArchive{ - IosArchive: v1xcarchive.IosArchive{ - Application: v1xcarchive.IosApplication{ - IosBaseApplication: v1xcarchive.IosBaseApplication{ + Application: IosApplication{ + IosBaseApplication: IosBaseApplication{ + InfoPlist: map[string]interface{}{ + "CFBundleIdentifier": "io.bitrise.app", + "DTPlatformName": "iphoneos", + }, + ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ + TeamID: "1234ASDF", + }, + }, + WatchApplication: &IosWatchApplication{ + IosBaseApplication: IosBaseApplication{ InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.app", - "DTPlatformName": "iphoneos", + "CFBundleIdentifier": "io.bitrise.watchapp", + "DTPlatformName": "watchos", }, ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ TeamID: "1234ASDF", }, }, - WatchApplication: &v1xcarchive.IosWatchApplication{ - IosBaseApplication: v1xcarchive.IosBaseApplication{ - InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.watchapp", - "DTPlatformName": "watchos", - }, - ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ - TeamID: "1234ASDF", - }, - }, - Extensions: []v1xcarchive.IosExtension{ - { - IosBaseApplication: v1xcarchive.IosBaseApplication{ - InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.watch-widget", - "DTPlatformName": "watchos", - }, - ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ - TeamID: "1234ASDF", - }, + Extensions: []IosExtension{ + { + IosBaseApplication: IosBaseApplication{ + InfoPlist: map[string]interface{}{ + "CFBundleIdentifier": "io.bitrise.watch-widget", + "DTPlatformName": "watchos", + }, + ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ + TeamID: "1234ASDF", }, }, }, }, - ClipApplication: &v1xcarchive.IosClipApplication{ - IosBaseApplication: v1xcarchive.IosBaseApplication{ + }, + ClipApplication: &IosClipApplication{ + IosBaseApplication: IosBaseApplication{ + InfoPlist: map[string]interface{}{ + "CFBundleIdentifier": "io.bitrise.clip", + "DTPlatformName": "iphoneos", + }, + ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ + TeamID: "1234ASDF", + }, + }, + }, + Extensions: []IosExtension{ + { + IosBaseApplication: IosBaseApplication{ InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.clip", + "CFBundleIdentifier": "io.bitrise.ios-widget1", "DTPlatformName": "iphoneos", }, ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ @@ -89,27 +346,14 @@ func TestIosArchive_GetAppLayout(t *testing.T) { }, }, }, - Extensions: []v1xcarchive.IosExtension{ - { - IosBaseApplication: v1xcarchive.IosBaseApplication{ - InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.ios-widget1", - "DTPlatformName": "iphoneos", - }, - ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ - TeamID: "1234ASDF", - }, + { + IosBaseApplication: IosBaseApplication{ + InfoPlist: map[string]interface{}{ + "CFBundleIdentifier": "io.bitrise.ios-widget2", + "DTPlatformName": "iphoneos", }, - }, - { - IosBaseApplication: v1xcarchive.IosBaseApplication{ - InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.ios-widget2", - "DTPlatformName": "iphoneos", - }, - ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ - TeamID: "1234ASDF", - }, + ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ + TeamID: "1234ASDF", }, }, }, @@ -131,20 +375,18 @@ func TestIosArchive_GetAppLayout(t *testing.T) { { name: "Single target app with capabilities", archive: IosArchive{ - IosArchive: v1xcarchive.IosArchive{ - Application: v1xcarchive.IosApplication{ - IosBaseApplication: v1xcarchive.IosBaseApplication{ - InfoPlist: map[string]interface{}{ - "CFBundleIdentifier": "io.bitrise.app", - "DTPlatformName": "iphoneos", - }, - ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ - TeamID: "1234ASDF", - }, - Entitlements: map[string]interface{}{ - "get-task-allow": false, - "com.apple.security.application-groups": []string{"group.io.bitrise.app"}, - }, + Application: IosApplication{ + IosBaseApplication: IosBaseApplication{ + InfoPlist: map[string]interface{}{ + "CFBundleIdentifier": "io.bitrise.app", + "DTPlatformName": "iphoneos", + }, + ProvisioningProfile: profileutil.ProvisioningProfileInfoModel{ + TeamID: "1234ASDF", + }, + Entitlements: map[string]interface{}{ + "get-task-allow": false, + "com.apple.security.application-groups": []string{"group.io.bitrise.app"}, }, }, }, diff --git a/xcarchive/macos.go b/xcarchive/macos.go new file mode 100644 index 00000000..714e94a1 --- /dev/null +++ b/xcarchive/macos.go @@ -0,0 +1,231 @@ +package xcarchive + +import ( + "fmt" + "path/filepath" + + "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" + + "github.com/bitrise-io/go-utils/pathutil" +) + +type macosBaseApplication struct { + Path string + InfoPlist plistutil.PlistData + Entitlements plistutil.PlistData + ProvisioningProfile *profileutil.ProvisioningProfileInfoModel +} + +// BundleIdentifier ... +func (app macosBaseApplication) BundleIdentifier() string { + bundleID, _ := app.InfoPlist.GetString("CFBundleIdentifier") + return bundleID +} + +func newMacosBaseApplication(path string) (macosBaseApplication, error) { + var infoPlist plistutil.PlistData + { + infoPlistPath := filepath.Join(path, "Contents/Info.plist") + if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { + return macosBaseApplication{}, fmt.Errorf("failed to check if Info.plist exists at: %s, error: %s", infoPlistPath, err) + } else if !exist { + return macosBaseApplication{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) + } + plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + if err != nil { + return macosBaseApplication{}, err + } + infoPlist = plist + } + + var provisioningProfile *profileutil.ProvisioningProfileInfoModel + { + provisioningProfilePath := filepath.Join(path, "Contents/embedded.provisionprofile") + if exist, err := pathutil.IsPathExists(provisioningProfilePath); err != nil { + return macosBaseApplication{}, fmt.Errorf("failed to check if profile exists at: %s, error: %s", provisioningProfilePath, err) + } else if exist { + profile, err := profileutil.NewProvisioningProfileInfoFromFile(provisioningProfilePath) + if err != nil { + return macosBaseApplication{}, err + } + provisioningProfile = &profile + } + } + + executable := filepath.Join("/Contents/MacOS/", executableNameFromInfoPlist(infoPlist)) + entitlements, err := getEntitlements(path, executable) + if err != nil { + return macosBaseApplication{}, err + } + + return macosBaseApplication{ + Path: path, + InfoPlist: infoPlist, + Entitlements: entitlements, + ProvisioningProfile: provisioningProfile, + }, nil +} + +// MacosExtension ... +type MacosExtension struct { + macosBaseApplication +} + +// NewMacosExtension ... +func NewMacosExtension(path string) (MacosExtension, error) { + baseApp, err := newMacosBaseApplication(path) + if err != nil { + return MacosExtension{}, err + } + + return MacosExtension{ + baseApp, + }, nil +} + +// MacosApplication ... +type MacosApplication struct { + macosBaseApplication + Extensions []MacosExtension +} + +// NewMacosApplication ... +func NewMacosApplication(path string) (MacosApplication, error) { + baseApp, err := newMacosBaseApplication(path) + if err != nil { + return MacosApplication{}, err + } + + extensions := []MacosExtension{} + { + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "Contents/PlugIns/*.appex") + pths, err := filepath.Glob(pattern) + if err != nil { + return MacosApplication{}, fmt.Errorf("failed to search for watch application's extensions using pattern: %s, error: %s", pattern, err) + } + for _, pth := range pths { + extension, err := NewMacosExtension(pth) + if err != nil { + return MacosApplication{}, err + } + + extensions = append(extensions, extension) + } + } + + return MacosApplication{ + macosBaseApplication: baseApp, + Extensions: extensions, + }, nil +} + +// MacosArchive ... +type MacosArchive struct { + Path string + InfoPlist plistutil.PlistData + Application MacosApplication +} + +// NewMacosArchive ... +func NewMacosArchive(path string) (MacosArchive, error) { + var infoPlist plistutil.PlistData + { + infoPlistPath := filepath.Join(path, "Info.plist") + if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { + return MacosArchive{}, fmt.Errorf("failed to check if Info.plist exists at: %s, error: %s", infoPlistPath, err) + } else if !exist { + return MacosArchive{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) + } + plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + if err != nil { + return MacosArchive{}, err + } + infoPlist = plist + } + + var application MacosApplication + { + pattern := filepath.Join(pathutil.EscapeGlobPath(path), "Products/Applications/*.app") + pths, err := filepath.Glob(pattern) + if err != nil { + return MacosArchive{}, err + } + + appPath := "" + if len(pths) > 0 { + appPath = pths[0] + } else { + return MacosArchive{}, fmt.Errorf("failed to find main app, using pattern: %s", pattern) + } + + app, err := NewMacosApplication(appPath) + if err != nil { + return MacosArchive{}, err + } + application = app + } + + return MacosArchive{ + Path: path, + InfoPlist: infoPlist, + Application: application, + }, nil +} + +// IsXcodeManaged ... +func (archive MacosArchive) IsXcodeManaged() bool { + if archive.Application.ProvisioningProfile != nil { + return archive.Application.ProvisioningProfile.IsXcodeManaged() + } + return false +} + +// SigningIdentity ... +func (archive MacosArchive) SigningIdentity() string { + properties, found := archive.InfoPlist.GetMapStringInterface("ApplicationProperties") + if found { + identity, _ := properties.GetString("SigningIdentity") + return identity + } + return "" +} + +// BundleIDEntitlementsMap ... +func (archive MacosArchive) BundleIDEntitlementsMap() map[string]plistutil.PlistData { + bundleIDEntitlementsMap := map[string]plistutil.PlistData{} + + bundleID := archive.Application.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = archive.Application.Entitlements + + for _, plugin := range archive.Application.Extensions { + bundleID := plugin.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = plugin.Entitlements + } + + return bundleIDEntitlementsMap +} + +// BundleIDProfileInfoMap ... +func (archive MacosArchive) BundleIDProfileInfoMap() map[string]profileutil.ProvisioningProfileInfoModel { + bundleIDProfileMap := map[string]profileutil.ProvisioningProfileInfoModel{} + + if archive.Application.ProvisioningProfile != nil { + bundleID := archive.Application.BundleIdentifier() + bundleIDProfileMap[bundleID] = *archive.Application.ProvisioningProfile + } + + for _, plugin := range archive.Application.Extensions { + if plugin.ProvisioningProfile != nil { + bundleID := plugin.BundleIdentifier() + bundleIDProfileMap[bundleID] = *plugin.ProvisioningProfile + } + } + + return bundleIDProfileMap +} + +// FindDSYMs ... +func (archive MacosArchive) FindDSYMs() ([]string, []string, error) { + return findDSYMs(archive.Path) +} diff --git a/xcarchive/macos_test.go b/xcarchive/macos_test.go new file mode 100644 index 00000000..335af258 --- /dev/null +++ b/xcarchive/macos_test.go @@ -0,0 +1,78 @@ +package xcarchive + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewMacosArchive(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + require.Equal(t, 5, len(archive.InfoPlist)) + + app := archive.Application + require.Equal(t, 21, len(app.InfoPlist)) + require.Equal(t, 2, len(app.Entitlements)) + require.Nil(t, app.ProvisioningProfile) + + require.Equal(t, 1, len(app.Extensions)) + extension := app.Extensions[0] + require.Equal(t, 22, len(extension.InfoPlist)) + require.Equal(t, 2, len(extension.Entitlements)) + require.Nil(t, extension.ProvisioningProfile) +} + +func TestMacosIsXcodeManaged(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + require.Equal(t, false, archive.IsXcodeManaged()) +} + +func TestMacosSigningIdentity(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + require.Equal(t, "Mac Developer: Gödrei Krisztian (T3694PR6UJ)", archive.SigningIdentity()) +} + +func TestMacosBundleIDEntitlementsMap(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + bundleIDEntitlementsMap := archive.BundleIDEntitlementsMap() + require.Equal(t, 2, len(bundleIDEntitlementsMap)) + + bundleIDs := []string{"io.bitrise.archive.Test", "io.bitrise.archive.Test.ActionExtension"} + for _, bundleID := range bundleIDs { + _, ok := bundleIDEntitlementsMap[bundleID] + require.True(t, ok, fmt.Sprintf("%v", bundleIDEntitlementsMap)) + } +} + +func TestMacosBundleIDProfileInfoMap(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + bundleIDProfileInfoMap := archive.BundleIDProfileInfoMap() + require.Equal(t, 0, len(bundleIDProfileInfoMap)) +} + +func TestMacosFindDSYMs(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + appDsym, otherDsyms, err := archive.FindDSYMs() + require.NoError(t, err) + require.Equal(t, 1, len(appDsym)) + require.Equal(t, 1, len(otherDsyms)) +} diff --git a/xcarchive/xcarchive.go b/xcarchive/xcarchive.go new file mode 100644 index 00000000..9565ceef --- /dev/null +++ b/xcarchive/xcarchive.go @@ -0,0 +1,88 @@ +package xcarchive + +import ( + "path/filepath" + "strings" + + "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-utils/ziputil" + "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/utility" +) + +// IsMacOS try to find the Contents dir under the .app/. +// If its finds it the archive is MacOs. If it does not the archive is iOS. +func IsMacOS(archPath string) (bool, error) { + log.Debugf("Checking archive is MacOS or iOS") + infoPlistPath := filepath.Join(archPath, "Info.plist") + + plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + if err != nil { + return false, err + } + + appProperties, found := plist.GetMapStringInterface("ApplicationProperties") + if !found { + return false, err + } + + applicationPath, found := appProperties.GetString("ApplicationPath") + if !found { + return false, err + } + + applicationPath = filepath.Join(archPath, "Products", applicationPath) + contentsPath := filepath.Join(applicationPath, "Contents") + + exist, err := pathutil.IsDirExists(contentsPath) + if err != nil { + return false, err + } + + return exist, nil +} + +// UnzipXcarchive ... +func UnzipXcarchive(xcarchivePth string) (string, error) { + tmpDir, err := pathutil.NormalizedOSTempDirPath("__xcarhive__") + if err != nil { + return "", err + } + + return tmpDir, ziputil.UnZip(xcarchivePth, tmpDir) +} + +// GetEmbeddedMobileProvisionPath ... +func GetEmbeddedMobileProvisionPath(xcarchivePth string) (string, error) { + return utility.FindFileInAppDir(getAppSubfolder(xcarchivePth), "embedded.mobileprovision") +} + +// GetEmbeddedInfoPlistPath ... +func GetEmbeddedInfoPlistPath(xcarchivePth string) (string, error) { + return utility.FindFileInAppDir(getAppSubfolder(xcarchivePth), "Info.plist") +} + +func getAppSubfolder(basepth string) string { + return filepath.Join(basepth, "Products", "Applications") +} + +func findDSYMs(archivePath string) ([]string, []string, error) { + dsymsDirPth := filepath.Join(archivePath, "dSYMs") + dsyms, err := pathutil.ListEntries(dsymsDirPth, pathutil.ExtensionFilter(".dsym", true)) + if err != nil { + return []string{}, []string{}, err + } + + appDSYMs := []string{} + frameworkDSYMs := []string{} + for _, dsym := range dsyms { + if strings.HasSuffix(dsym, ".app.dSYM") { + appDSYMs = append(appDSYMs, dsym) + } else { + frameworkDSYMs = append(frameworkDSYMs, dsym) + } + } + + return appDSYMs, frameworkDSYMs, nil +} diff --git a/xcarchive/xcarchive_test.go b/xcarchive/xcarchive_test.go new file mode 100644 index 00000000..70b5d867 --- /dev/null +++ b/xcarchive/xcarchive_test.go @@ -0,0 +1,166 @@ +package xcarchive + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/bitrise-io/go-utils/pathutil" + "github.com/stretchr/testify/assert" +) + +const ( + tempDirName = "__artifacts__" + DSYMSDirName = "dSYMs" +) + +func TestIsMacOS(t *testing.T) { + tests := []struct { + name string + archPath string + want bool + wantErr bool + }{ + { + name: "macOS", + archPath: filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive"), + want: true, + wantErr: false, + }, + { + name: "iOS", + archPath: filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive"), + want: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := IsMacOS(tt.archPath) + if (err != nil) != tt.wantErr { + t.Errorf("IsMacOS() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("IsMacOS() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_GivenArchiveWithMultipleAppAndFrameworkDSYMs_WhenFindDSYMsCalled_ThenExpectAllDSYMsToBeReturned(t *testing.T) { + testCases := []struct { + name string + numberOfAppDSYMs int + numberOfFrameworkDSYMs int + }{ + { + name: "1. Given archive with multiple app and framework dSYMs when FindDSYMs called then expect all dSYMs to be returned", + numberOfAppDSYMs: 2, + numberOfFrameworkDSYMs: 2, + }, + { + name: "2. Given archive with singe app and framework dSYMs when FindDSYMs called then expect both dSYMs to be returned", + numberOfAppDSYMs: 1, + numberOfFrameworkDSYMs: 1, + }, + { + name: "3. Given archive with multiple app dSYMs when FindDSYMs called then expect all app dSYMs to be returned", + numberOfAppDSYMs: 2, + numberOfFrameworkDSYMs: 0, + }, + { + name: "4. Given archive with multiple framework dSYMs when FindDSYMs called then expect all framework dSYMs to be returned", + numberOfAppDSYMs: 0, + numberOfFrameworkDSYMs: 2, + }, + { + name: "5. Given archive with single app dSYM when FindDSYMs called then expect the app dSYM to be returned", + numberOfAppDSYMs: 1, + numberOfFrameworkDSYMs: 0, + }, + { + name: "6. Given archive with single framework dSYM when FindDSYMs called then expect the framework dSYM to be returned", + numberOfAppDSYMs: 0, + numberOfFrameworkDSYMs: 1, + }, + { + name: "7. Given archive without any dSYM when FindDSYMs called then expect no dSYM to be returned", + numberOfAppDSYMs: 0, + numberOfFrameworkDSYMs: 0, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + archivePath, err := createArchiveWithAppAndFrameworkDSYMs( + "archives/ios.dsyms.xcarchive", + testCase.numberOfAppDSYMs, + testCase.numberOfFrameworkDSYMs, + ) + assert.NoError(t, err) + + appDSYMs, frameworkDSYMs, err := findDSYMs(archivePath) + assert.NoError(t, err) + assert.Equal(t, testCase.numberOfAppDSYMs, len(appDSYMs)) + assert.Equal(t, testCase.numberOfFrameworkDSYMs, len(frameworkDSYMs)) + }) + } +} + +func createArchiveWithAppAndFrameworkDSYMs(archivePath string, numberOfAppDSYMs, numberOfFrameworkDSYMs int) (string, error) { + archivePath, err := createArchive(archivePath) + if err != nil { + return "", err + } + + err = createAppDSYMs(archivePath, numberOfAppDSYMs) + if err != nil { + return "", err + } + + err = createFrameworkDSYMs(archivePath, numberOfFrameworkDSYMs) + if err != nil { + return "", err + } + + return archivePath, nil +} + +func createAppDSYMs(archivePath string, numberOfDSYMs int) error { + return createDSYMs(archivePath, "app", numberOfDSYMs) +} + +func createFrameworkDSYMs(archivePath string, numberOfDSYMs int) error { + return createDSYMs(archivePath, "framework", numberOfDSYMs) +} + +func createDSYMs(archivePath, dSYMType string, numberOfDSYMs int) error { + for i := 0; i < numberOfDSYMs; i++ { + err := os.WriteFile(createDSYMFilePath(archivePath, dSYMType, i), nil, 0777) + if err != nil { + return err + } + } + + return nil +} + +func createDSYMFilePath(archivePath, dSYMType string, index int) string { + return filepath.Join(archivePath, DSYMSDirName, fmt.Sprintf("ios-%d.%s.dSYM", index, dSYMType)) +} + +func createArchive(archivePath string) (string, error) { + tempDirPath, err := pathutil.NormalizedOSTempDirPath(tempDirName) + if err != nil { + return "", err + } + + archivePath = filepath.Join(tempDirPath, archivePath) + err = os.MkdirAll(filepath.Join(archivePath, DSYMSDirName), 0755) + if err != nil { + return "", err + } + + return archivePath, nil +} From 5ea8e3cc3afc6f9fba7705ac3ac3adfb6ca887e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krisztia=CC=81n=20Go=CC=88drei?= Date: Fri, 20 Jun 2025 14:30:06 +0200 Subject: [PATCH 4/7] Remove unused xcarchive functionalities and move exportoptions to v2 --- .../localcodesignasset_test.go | 2 +- .../localcodesignasset/profilelookup_test.go | 2 +- autocodesign/projectmanager/projecthelper.go | 2 +- export/filter.go | 2 +- export/mac.go | 2 +- exportoptions/appstore_options.go | 115 +++++ exportoptions/exportoptions.go | 46 ++ exportoptions/exportoptions_test.go | 440 ++++++++++++++++++ exportoptions/non_appstore_options.go | 103 ++++ exportoptions/properties.go | 225 +++++++++ exportoptions/properties_test.go | 45 ++ .../exportoptionsgenerator.go | 2 +- .../exportoptionsgenerator_test.go | 2 +- metaparser/metaparser.go | 2 +- profileutil/info_model.go | 4 +- profileutil/plist_data.go | 4 +- profileutil/plist_data_test.go | 2 +- xcarchive/entitlements_test.go | 74 --- xcarchive/{entitlements.go => utils.go} | 22 + .../{xcarchive_test.go => utils_test.go} | 90 ++-- xcarchive/xcarchive.go | 88 ---- 21 files changed, 1069 insertions(+), 205 deletions(-) create mode 100644 exportoptions/appstore_options.go create mode 100644 exportoptions/exportoptions.go create mode 100644 exportoptions/exportoptions_test.go create mode 100644 exportoptions/non_appstore_options.go create mode 100644 exportoptions/properties.go create mode 100644 exportoptions/properties_test.go delete mode 100644 xcarchive/entitlements_test.go rename xcarchive/{entitlements.go => utils.go} (65%) rename xcarchive/{xcarchive_test.go => utils_test.go} (64%) delete mode 100644 xcarchive/xcarchive.go diff --git a/autocodesign/localcodesignasset/localcodesignasset_test.go b/autocodesign/localcodesignasset/localcodesignasset_test.go index 10bc25dc..406a23c1 100644 --- a/autocodesign/localcodesignasset/localcodesignasset_test.go +++ b/autocodesign/localcodesignasset/localcodesignasset_test.go @@ -6,11 +6,11 @@ import ( devportaltime "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/time" - "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" "github.com/bitrise-io/go-xcode/v2/autocodesign/localcodesignasset/mocks" "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/exportoptions" "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" diff --git a/autocodesign/localcodesignasset/profilelookup_test.go b/autocodesign/localcodesignasset/profilelookup_test.go index 83e3aaa9..9aad5716 100644 --- a/autocodesign/localcodesignasset/profilelookup_test.go +++ b/autocodesign/localcodesignasset/profilelookup_test.go @@ -3,9 +3,9 @@ package localcodesignasset import ( "testing" - "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/exportoptions" "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/stretchr/testify/assert" ) diff --git a/autocodesign/projectmanager/projecthelper.go b/autocodesign/projectmanager/projecthelper.go index 8f3a33ac..27ac5d65 100644 --- a/autocodesign/projectmanager/projecthelper.go +++ b/autocodesign/projectmanager/projecthelper.go @@ -8,6 +8,7 @@ import ( "regexp" "strings" + "github.com/bitrise-io/go-plist" "github.com/bitrise-io/go-utils/fileutil" "github.com/bitrise-io/go-utils/log" "github.com/bitrise-io/go-utils/pathutil" @@ -17,7 +18,6 @@ import ( "github.com/bitrise-io/go-xcode/xcodeproject/serialized" "github.com/bitrise-io/go-xcode/xcodeproject/xcodeproj" "github.com/bitrise-io/go-xcode/xcodeproject/xcscheme" - "howett.net/plist" ) // ProjectHelper ... diff --git a/export/filter.go b/export/filter.go index d20e9fb2..53b19632 100644 --- a/export/filter.go +++ b/export/filter.go @@ -2,8 +2,8 @@ package export import ( "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/v2/exportoptions" "github.com/bitrise-io/go-xcode/v2/profileutil" ) diff --git a/export/mac.go b/export/mac.go index b2ebb063..73134a48 100644 --- a/export/mac.go +++ b/export/mac.go @@ -1,8 +1,8 @@ package export import ( - "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/exportoptions" "github.com/bitrise-io/go-xcode/v2/profileutil" ) diff --git a/exportoptions/appstore_options.go b/exportoptions/appstore_options.go new file mode 100644 index 00000000..c4e4eb5c --- /dev/null +++ b/exportoptions/appstore_options.go @@ -0,0 +1,115 @@ +package exportoptions + +import ( + "fmt" + + "github.com/bitrise-io/go-plist" +) + +// AppStoreOptionsModel ... +type AppStoreOptionsModel struct { + Method Method + TeamID string + BundleIDProvisioningProfileMapping map[string]string + SigningCertificate string + InstallerSigningCertificate string + SigningStyle SigningStyle + Destination Destination + ICloudContainerEnvironment ICloudContainerEnvironment + DistributionBundleIdentifier string + + // for app-store exports + UploadBitcode bool + UploadSymbols bool + // Should Xcode manage the app's build number when uploading to App Store Connect? Defaults to YES. + ManageAppVersion bool + + TestFlightInternalTestingOnly bool +} + +// NewAppStoreOptions sets "app-store" as the export method +// deprecated: use NewAppStoreConnectOptions instead +func NewAppStoreOptions() AppStoreOptionsModel { + return NewAppStoreConnectOptions(MethodAppStore) +} + +// NewAppStoreConnectOptions sets either "app-store" or "app-store-connect" as the export method +func NewAppStoreConnectOptions(method Method) AppStoreOptionsModel { + if !method.IsAppStore() { + panic("non app-store method passed to NewAppStoreConnectOptions") + } + return AppStoreOptionsModel{ + Method: method, + UploadBitcode: UploadBitcodeDefault, + UploadSymbols: UploadSymbolsDefault, + ManageAppVersion: manageAppVersionDefault, + TestFlightInternalTestingOnly: TestFlightInternalTestingOnlyDefault, + } +} + +// Hash ... +func (options AppStoreOptionsModel) Hash() map[string]interface{} { + hash := map[string]interface{}{} + hash[MethodKey] = options.Method + if options.TeamID != "" { + hash[TeamIDKey] = options.TeamID + } + //nolint:gosimple + if options.UploadBitcode != UploadBitcodeDefault { + hash[UploadBitcodeKey] = options.UploadBitcode + } + //nolint:gosimple + if options.UploadSymbols != UploadSymbolsDefault { + hash[UploadSymbolsKey] = options.UploadSymbols + } + //nolint:gosimple + if options.ManageAppVersion != manageAppVersionDefault { + hash[manageAppVersionKey] = options.ManageAppVersion + } + if options.ICloudContainerEnvironment != "" { + hash[ICloudContainerEnvironmentKey] = options.ICloudContainerEnvironment + } + if options.DistributionBundleIdentifier != "" { + hash[DistributionBundleIdentifier] = options.DistributionBundleIdentifier + } + if len(options.BundleIDProvisioningProfileMapping) > 0 { + hash[ProvisioningProfilesKey] = options.BundleIDProvisioningProfileMapping + } + if options.SigningCertificate != "" { + hash[SigningCertificateKey] = options.SigningCertificate + } + if options.InstallerSigningCertificate != "" { + hash[InstallerSigningCertificateKey] = options.InstallerSigningCertificate + } + if options.SigningStyle != "" { + hash[SigningStyleKey] = options.SigningStyle + } + if options.Destination != "" { + hash[DestinationKey] = options.Destination + } + //nolint:gosimple + if options.TestFlightInternalTestingOnly != TestFlightInternalTestingOnlyDefault { + hash[TestFlightInternalTestingOnlyKey] = options.TestFlightInternalTestingOnly + } + return hash +} + +// String ... +func (options AppStoreOptionsModel) String() (string, error) { + hash := options.Hash() + plistBytes, err := plist.MarshalIndent(hash, plist.XMLFormat, "\t") + if err != nil { + return "", fmt.Errorf("failed to marshal export options model, error: %s", err) + } + return string(plistBytes), err +} + +// WriteToFile ... +func (options AppStoreOptionsModel) WriteToFile(pth string) error { + return WritePlistToFile(options.Hash(), pth) +} + +// WriteToTmpFile ... +func (options AppStoreOptionsModel) WriteToTmpFile() (string, error) { + return WritePlistToTmpFile(options.Hash()) +} diff --git a/exportoptions/exportoptions.go b/exportoptions/exportoptions.go new file mode 100644 index 00000000..23aa5573 --- /dev/null +++ b/exportoptions/exportoptions.go @@ -0,0 +1,46 @@ +package exportoptions + +import ( + "fmt" + "path/filepath" + + "github.com/bitrise-io/go-plist" + "github.com/bitrise-io/go-utils/fileutil" + "github.com/bitrise-io/go-utils/pathutil" +) + +// ExportOptions ... +type ExportOptions interface { + Hash() map[string]interface{} + String() (string, error) + WriteToFile(pth string) error + WriteToTmpFile() (string, error) +} + +// WritePlistToFile ... +func WritePlistToFile(options map[string]interface{}, pth string) error { + plistBytes, err := plist.MarshalIndent(options, plist.XMLFormat, "\t") + if err != nil { + return fmt.Errorf("failed to marshal export options model, error: %s", err) + } + if err := fileutil.WriteBytesToFile(pth, plistBytes); err != nil { + return fmt.Errorf("failed to write export options, error: %s", err) + } + + return nil +} + +// WritePlistToTmpFile ... +func WritePlistToTmpFile(options map[string]interface{}) (string, error) { + tmpDir, err := pathutil.NormalizedOSTempDirPath("output") + if err != nil { + return "", fmt.Errorf("failed to create temp dir, error: %s", err) + } + pth := filepath.Join(tmpDir, "exportOptions.plist") + + if err := WritePlistToFile(options, pth); err != nil { + return "", fmt.Errorf("failed to write to file options, error: %s", err) + } + + return pth, nil +} diff --git a/exportoptions/exportoptions_test.go b/exportoptions/exportoptions_test.go new file mode 100644 index 00000000..51be8c89 --- /dev/null +++ b/exportoptions/exportoptions_test.go @@ -0,0 +1,440 @@ +package exportoptions + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/bitrise-io/go-utils/fileutil" + "github.com/bitrise-io/go-utils/pathutil" + "github.com/stretchr/testify/require" +) + +func TestManifestIsEmpty(t *testing.T) { + t.Log("returns true if empty manifest") + { + manifest := Manifest{} + require.Equal(t, true, manifest.IsEmpty()) + } + + t.Log("returns false if not empty manifest") + { + manifest := Manifest{ + AppURL: "appURL", + } + require.Equal(t, false, manifest.IsEmpty()) + } + { + manifest := Manifest{ + DisplayImageURL: "displayImageURL", + } + require.Equal(t, false, manifest.IsEmpty()) + } + { + manifest := Manifest{ + FullSizeImageURL: "fullSizeImageURL.", + } + require.Equal(t, false, manifest.IsEmpty()) + } + { + manifest := Manifest{ + AssetPackManifestURL: "assetPackManifestURL.", + } + require.Equal(t, false, manifest.IsEmpty()) + } +} + +func TestManifestToHash(t *testing.T) { + t.Log("empty manifest creates empty hash") + { + manifest := Manifest{} + hash := manifest.ToHash() + require.Equal(t, 0, len(hash)) + { + value, ok := hash[ManifestAppURLKey] + require.Equal(t, false, ok) + require.Equal(t, "", value) + } + { + value, ok := hash[ManifestDisplayImageURLKey] + require.Equal(t, false, ok) + require.Equal(t, "", value) + } + { + value, ok := hash[ManifestFullSizeImageURLKey] + require.Equal(t, false, ok) + require.Equal(t, "", value) + } + { + value, ok := hash[ManifestAssetPackManifestURLKey] + require.Equal(t, false, ok) + require.Equal(t, "", value) + } + } + + t.Log("creates hash from manifest") + { + manifest := Manifest{ + AppURL: "appURL", + DisplayImageURL: "displayImageURL", + FullSizeImageURL: "fullSizeImageURL", + AssetPackManifestURL: "assetPackManifestURL", + } + hash := manifest.ToHash() + require.Equal(t, 4, len(hash)) + { + value, ok := hash[ManifestAppURLKey] + require.Equal(t, true, ok) + require.Equal(t, "appURL", value) + } + { + value, ok := hash[ManifestDisplayImageURLKey] + require.Equal(t, true, ok) + require.Equal(t, "displayImageURL", value) + } + { + value, ok := hash[ManifestFullSizeImageURLKey] + require.Equal(t, true, ok) + require.Equal(t, "fullSizeImageURL", value) + } + { + value, ok := hash[ManifestAssetPackManifestURLKey] + require.Equal(t, true, ok) + require.Equal(t, "assetPackManifestURL", value) + } + } +} + +func TestNewAppStoreConnectOptions(t *testing.T) { + t.Log("create app-store type export options with default values") + { + options := NewAppStoreConnectOptions(MethodAppStoreConnect) + require.Equal(t, UploadBitcodeDefault, options.UploadBitcode) + require.Equal(t, UploadSymbolsDefault, options.UploadSymbols) + require.Equal(t, TestFlightInternalTestingOnlyDefault, options.TestFlightInternalTestingOnly) + } +} + +func TestAppStoreOptionsToHash(t *testing.T) { + t.Log("default app-store type options creates hash with legacy method") + { + options := NewAppStoreOptions() + options.ManageAppVersion = true + hash := options.Hash() + require.Equal(t, 1, len(hash), fmt.Sprintf("Hash: %+v", hash)) + + { + value, ok := hash[MethodKey] + require.Equal(t, true, ok) + require.Equal(t, MethodAppStore, value) + } + } + + t.Log("default app-store type options creates hash with new method") + { + options := NewAppStoreConnectOptions(MethodAppStoreConnect) + options.ManageAppVersion = true + hash := options.Hash() + require.Equal(t, 1, len(hash), fmt.Sprintf("Hash: %+v", hash)) + + { + value, ok := hash[MethodKey] + require.Equal(t, true, ok) + require.Equal(t, MethodAppStoreConnect, value) + } + } + + t.Log("custom app-store type option's generated hash contains all properties") + { + options := NewAppStoreOptions() + options.TeamID = "123" + options.UploadBitcode = false + options.UploadSymbols = false + options.ManageAppVersion = false + options.TestFlightInternalTestingOnly = true + + hash := options.Hash() + require.Equal(t, 6, len(hash)) + + { + value, ok := hash[MethodKey] + require.True(t, ok) + require.Equal(t, MethodAppStore, value) + } + { + value, ok := hash[TeamIDKey] + require.True(t, ok) + require.Equal(t, "123", value) + } + { + value, ok := hash[UploadBitcodeKey] + require.True(t, ok) + require.Equal(t, false, value) + } + { + value, ok := hash[UploadSymbolsKey] + require.True(t, ok) + require.Equal(t, false, value) + } + { + value, ok := hash[manageAppVersionKey] + require.True(t, ok) + require.Equal(t, false, value) + } + { + value, ok := hash[TestFlightInternalTestingOnlyKey] + require.True(t, ok) + require.Equal(t, true, value) + } + } +} + +func TestAppStoreOptionsWriteToFile(t *testing.T) { + t.Log("default app-store type options overrides only method") + { + tmpDir, err := pathutil.NormalizedOSTempDirPath("output") + require.NoError(t, err) + pth := filepath.Join(tmpDir, "exportOptions.plist") + + options := NewAppStoreConnectOptions(MethodAppStoreConnect) + options.ManageAppVersion = true + require.NoError(t, options.WriteToFile(pth)) + + content, err := fileutil.ReadStringFromFile(pth) + require.NoError(t, err) + desired := ` + + + + method + app-store-connect + +` + require.Equal(t, desired, content) + } + + t.Log("custom app-store type options overrides all properties") + { + tmpDir, err := pathutil.NormalizedOSTempDirPath("output") + require.NoError(t, err) + pth := filepath.Join(tmpDir, "exportOptions.plist") + + options := NewAppStoreOptions() + options.TeamID = "123" + options.UploadBitcode = false + options.UploadSymbols = false + options.ManageAppVersion = false + require.NoError(t, options.WriteToFile(pth)) + + content, err := fileutil.ReadStringFromFile(pth) + require.NoError(t, err) + desired := ` + + + + manageAppVersionAndBuildNumber + + method + app-store + teamID + 123 + uploadBitcode + + uploadSymbols + + +` + require.Equal(t, desired, content) + } +} + +func TestNonNewAppStoreOptions(t *testing.T) { + t.Log("create NON app-store type export options with default values") + { + options := NewNonAppStoreOptions(MethodDevelopment) + require.Equal(t, MethodDevelopment, options.Method) + require.Equal(t, CompileBitcodeDefault, options.CompileBitcode) + require.Equal(t, EmbedOnDemandResourcesAssetPacksInBundleDefault, options.EmbedOnDemandResourcesAssetPacksInBundle) + require.Equal(t, ICloudContainerEnvironment(""), options.ICloudContainerEnvironment) + require.Equal(t, ThinningDefault, options.Thinning) + } +} + +func TestNonAppStoreOptionsToHash(t *testing.T) { + t.Log("default NON app-store type options creates hash with method") + { + options := NewNonAppStoreOptions(MethodDevelopment) + hash := options.Hash() + require.Equal(t, 1, len(hash)) + + { + value, ok := hash[MethodKey] + require.Equal(t, true, ok) + require.Equal(t, MethodDevelopment, value) + } + } + + t.Log("custom NON app-store type option's generated hash contains all properties") + { + options := NewNonAppStoreOptions(MethodEnterprise) + options.TeamID = "123" + options.CompileBitcode = false + options.EmbedOnDemandResourcesAssetPacksInBundle = false + options.ICloudContainerEnvironment = ICloudContainerEnvironmentProduction + options.OnDemandResourcesAssetPacksBaseURL = "url" + options.Thinning = ThinningThinForAllVariants + options.Manifest = Manifest{ + AppURL: "appURL", + DisplayImageURL: "displayImageURL", + FullSizeImageURL: "fullSizeImageURL", + AssetPackManifestURL: "assetPackManifestURL", + } + + hash := options.Hash() + require.Equal(t, 8, len(hash)) + + { + value, ok := hash[MethodKey] + require.Equal(t, true, ok) + require.Equal(t, MethodEnterprise, value) + } + { + value, ok := hash[TeamIDKey] + require.Equal(t, true, ok) + require.Equal(t, "123", value) + } + { + value, ok := hash[CompileBitcodeKey] + require.Equal(t, true, ok) + require.Equal(t, false, value) + } + { + value, ok := hash[EmbedOnDemandResourcesAssetPacksInBundleKey] + require.Equal(t, true, ok) + require.Equal(t, false, value) + } + { + value, ok := hash[ICloudContainerEnvironmentKey] + require.Equal(t, true, ok) + require.Equal(t, ICloudContainerEnvironmentProduction, value) + } + { + value, ok := hash[OnDemandResourcesAssetPacksBaseURLKey] + require.Equal(t, true, ok) + require.Equal(t, "url", value) + } + { + value, ok := hash[ThinningKey] + require.Equal(t, true, ok) + require.Equal(t, ThinningThinForAllVariants, value) + } + { + manifestHash, ok := hash[ManifestKey].(map[string]string) + require.Equal(t, true, ok) + require.Equal(t, 4, len(manifestHash)) + + { + value, ok := manifestHash[ManifestAppURLKey] + require.Equal(t, true, ok) + require.Equal(t, "appURL", value) + } + { + value, ok := manifestHash[ManifestDisplayImageURLKey] + require.Equal(t, true, ok) + require.Equal(t, "displayImageURL", value) + } + { + value, ok := manifestHash[ManifestFullSizeImageURLKey] + require.Equal(t, true, ok) + require.Equal(t, "fullSizeImageURL", value) + } + { + value, ok := manifestHash[ManifestAssetPackManifestURLKey] + require.Equal(t, true, ok) + require.Equal(t, "assetPackManifestURL", value) + } + } + } +} + +func TestNonAppStoreOptionsWriteToFile(t *testing.T) { + t.Log("default NON app-store type options overrides only method") + { + tmpDir, err := pathutil.NormalizedOSTempDirPath("output") + require.NoError(t, err) + pth := filepath.Join(tmpDir, "exportOptions.plist") + + options := NewNonAppStoreOptions(MethodEnterprise) + require.NoError(t, options.WriteToFile(pth)) + + content, err := fileutil.ReadStringFromFile(pth) + require.NoError(t, err) + desired := ` + + + + method + enterprise + +` + require.Equal(t, desired, content) + } + + t.Log("custom app-store type options overrides all properties") + { + tmpDir, err := pathutil.NormalizedOSTempDirPath("output") + require.NoError(t, err) + pth := filepath.Join(tmpDir, "exportOptions.plist") + + options := NewNonAppStoreOptions(MethodEnterprise) + options.TeamID = "123" + options.CompileBitcode = false + options.EmbedOnDemandResourcesAssetPacksInBundle = false + options.ICloudContainerEnvironment = ICloudContainerEnvironmentProduction + options.OnDemandResourcesAssetPacksBaseURL = "url" + options.Thinning = ThinningThinForAllVariants + options.Manifest = Manifest{ + AppURL: "appURL", + DisplayImageURL: "displayImageURL", + FullSizeImageURL: "fullSizeImageURL", + AssetPackManifestURL: "assetPackManifestURL", + } + + require.NoError(t, options.WriteToFile(pth)) + + content, err := fileutil.ReadStringFromFile(pth) + require.NoError(t, err) + desired := ` + + + + compileBitcode + + embedOnDemandResourcesAssetPacksInBundle + + iCloudContainerEnvironment + Production + manifest + + appURL + appURL + assetPackManifestURL + assetPackManifestURL + displayImageURL + displayImageURL + fullSizeImageURL + fullSizeImageURL + + method + enterprise + onDemandResourcesAssetPacksBaseURL + url + teamID + 123 + thinning + thin-for-all-variants + +` + require.Equal(t, desired, content) + } +} diff --git a/exportoptions/non_appstore_options.go b/exportoptions/non_appstore_options.go new file mode 100644 index 00000000..c4b1b93d --- /dev/null +++ b/exportoptions/non_appstore_options.go @@ -0,0 +1,103 @@ +package exportoptions + +import ( + "fmt" + + "github.com/bitrise-io/go-plist" +) + +// NonAppStoreOptionsModel ... +type NonAppStoreOptionsModel struct { + Method Method + TeamID string + BundleIDProvisioningProfileMapping map[string]string + SigningCertificate string + SigningStyle SigningStyle + Destination Destination + ICloudContainerEnvironment ICloudContainerEnvironment + DistributionBundleIdentifier string + + // for non app-store exports + CompileBitcode bool + EmbedOnDemandResourcesAssetPacksInBundle bool + Manifest Manifest + OnDemandResourcesAssetPacksBaseURL string + Thinning string +} + +// NewNonAppStoreOptions ... +func NewNonAppStoreOptions(method Method) NonAppStoreOptionsModel { + return NonAppStoreOptionsModel{ + Method: method, + CompileBitcode: CompileBitcodeDefault, + EmbedOnDemandResourcesAssetPacksInBundle: EmbedOnDemandResourcesAssetPacksInBundleDefault, + Thinning: ThinningDefault, + } +} + +// Hash ... +func (options NonAppStoreOptionsModel) Hash() map[string]interface{} { + hash := map[string]interface{}{} + if options.Method != "" { + hash[MethodKey] = options.Method + } + if options.TeamID != "" { + hash[TeamIDKey] = options.TeamID + } + //nolint:gosimple + if options.CompileBitcode != CompileBitcodeDefault { + hash[CompileBitcodeKey] = options.CompileBitcode + } + //nolint:gosimple + if options.EmbedOnDemandResourcesAssetPacksInBundle != EmbedOnDemandResourcesAssetPacksInBundleDefault { + hash[EmbedOnDemandResourcesAssetPacksInBundleKey] = options.EmbedOnDemandResourcesAssetPacksInBundle + } + if options.ICloudContainerEnvironment != "" { + hash[ICloudContainerEnvironmentKey] = options.ICloudContainerEnvironment + } + if options.DistributionBundleIdentifier != "" { + hash[DistributionBundleIdentifier] = options.DistributionBundleIdentifier + } + if !options.Manifest.IsEmpty() { + hash[ManifestKey] = options.Manifest.ToHash() + } + if options.OnDemandResourcesAssetPacksBaseURL != "" { + hash[OnDemandResourcesAssetPacksBaseURLKey] = options.OnDemandResourcesAssetPacksBaseURL + } + if options.Thinning != ThinningDefault { + hash[ThinningKey] = options.Thinning + } + if len(options.BundleIDProvisioningProfileMapping) > 0 { + hash[ProvisioningProfilesKey] = options.BundleIDProvisioningProfileMapping + } + if options.SigningCertificate != "" { + hash[SigningCertificateKey] = options.SigningCertificate + } + if options.SigningStyle != "" { + hash[SigningStyleKey] = options.SigningStyle + } + if options.Destination != "" { + hash[DestinationKey] = options.Destination + } + return hash +} + +// String ... +func (options NonAppStoreOptionsModel) String() (string, error) { + hash := options.Hash() + plistBytes, err := plist.MarshalIndent(hash, plist.XMLFormat, "\t") + if err != nil { + return "", fmt.Errorf("failed to marshal export options model, error: %s", err) + } + return string(plistBytes), err +} + +// WriteToFile ... +func (options NonAppStoreOptionsModel) WriteToFile(pth string) error { + return WritePlistToFile(options.Hash(), pth) +} + +// WriteToTmpFile ... +func (options NonAppStoreOptionsModel) WriteToTmpFile() (string, error) { + return WritePlistToTmpFile(options.Hash()) +} diff --git a/exportoptions/properties.go b/exportoptions/properties.go new file mode 100644 index 00000000..86aaa72c --- /dev/null +++ b/exportoptions/properties.go @@ -0,0 +1,225 @@ +package exportoptions + +import "fmt" + +// CompileBitcodeKey ... +const CompileBitcodeKey = "compileBitcode" + +// CompileBitcodeDefault ... +const CompileBitcodeDefault = true + +// EmbedOnDemandResourcesAssetPacksInBundleKey ... +const EmbedOnDemandResourcesAssetPacksInBundleKey = "embedOnDemandResourcesAssetPacksInBundle" + +// EmbedOnDemandResourcesAssetPacksInBundleDefault ... +const EmbedOnDemandResourcesAssetPacksInBundleDefault = true + +// ICloudContainerEnvironmentKey ... +const ICloudContainerEnvironmentKey = "iCloudContainerEnvironment" + +// ICloudContainerEnvironment ... +type ICloudContainerEnvironment string + +const ( + // ICloudContainerEnvironmentDevelopment ... + ICloudContainerEnvironmentDevelopment ICloudContainerEnvironment = "Development" + // ICloudContainerEnvironmentProduction ... + ICloudContainerEnvironmentProduction ICloudContainerEnvironment = "Production" +) + +// DistributionBundleIdentifier ... +const DistributionBundleIdentifier = "distributionBundleIdentifier" + +// ManifestKey ... +const ManifestKey = "manifest" + +// ManifestAppURLKey ... +const ManifestAppURLKey = "appURL" + +// ManifestDisplayImageURLKey ... +const ManifestDisplayImageURLKey = "displayImageURL" + +// ManifestFullSizeImageURLKey ... +const ManifestFullSizeImageURLKey = "fullSizeImageURL" + +// ManifestAssetPackManifestURLKey ... +const ManifestAssetPackManifestURLKey = "assetPackManifestURL" + +// Manifest ... +type Manifest struct { + AppURL string + DisplayImageURL string + FullSizeImageURL string + AssetPackManifestURL string +} + +// IsEmpty ... +func (manifest Manifest) IsEmpty() bool { + return (manifest.AppURL == "" && manifest.DisplayImageURL == "" && manifest.FullSizeImageURL == "" && manifest.AssetPackManifestURL == "") +} + +// ToHash ... +func (manifest Manifest) ToHash() map[string]string { + hash := map[string]string{} + if manifest.AppURL != "" { + hash[ManifestAppURLKey] = manifest.AppURL + } + if manifest.DisplayImageURL != "" { + hash[ManifestDisplayImageURLKey] = manifest.DisplayImageURL + } + if manifest.FullSizeImageURL != "" { + hash[ManifestFullSizeImageURLKey] = manifest.FullSizeImageURL + } + if manifest.AssetPackManifestURL != "" { + hash[ManifestAssetPackManifestURLKey] = manifest.AssetPackManifestURL + } + return hash +} + +// MethodKey ... +const MethodKey = "method" + +// Method ... +type Method string + +const ( + // MethodAppStore is deprecated since Xcode 15.3, its new name is MethodAppStoreConnect + MethodAppStore Method = "app-store" + // MethodAdHoc is deprecated since Xcode 15.3, its new name is MethodReleaseTesting + MethodAdHoc Method = "ad-hoc" + // MethodPackage ... + MethodPackage Method = "package" + // MethodEnterprise ... + MethodEnterprise Method = "enterprise" + // MethodDevelopment is deprecated since Xcode 15.3, its new name is MethodDebugging + MethodDevelopment Method = "development" + // MethodDeveloperID ... + MethodDeveloperID Method = "developer-id" + // MethodDebugging is the new name for MethodDevelopment since Xcode 15.3 + MethodDebugging Method = "debugging" + // MethodAppStoreConnect is the new name for MethodAppStore since Xcode 15.3 + MethodAppStoreConnect Method = "app-store-connect" + // MethodReleaseTesting is the new name for MethodAdHoc since Xcode 15.3 + MethodReleaseTesting Method = "release-testing" + // MethodDefault ... + MethodDefault Method = MethodDevelopment +) + +func (m Method) IsAppStore() bool { + return m == MethodAppStore || m == MethodAppStoreConnect +} + +func (m Method) IsAdHoc() bool { + return m == MethodAdHoc || m == MethodReleaseTesting +} + +func (m Method) IsDevelopment() bool { + return m == MethodDevelopment || m == MethodDebugging +} + +func (m Method) IsEnterprise() bool { + return m == MethodEnterprise +} + +// ParseMethod parses Step input and returns the corresponding Method. +func ParseMethod(method string) (Method, error) { + switch method { + case "app-store": + return MethodAppStore, nil + case "ad-hoc": + return MethodAdHoc, nil + case "package": + return MethodPackage, nil + case "enterprise": + return MethodEnterprise, nil + case "development": + return MethodDevelopment, nil + case "developer-id": + return MethodDeveloperID, nil + default: + return Method(""), fmt.Errorf("unkown method (%s)", method) + } +} + +// UpgradeToXcode15_3MethodName replaces the legacy export method strings with the ones available in Xcode 15.3 and later. +func UpgradeToXcode15_3MethodName(method Method) Method { + switch method { + case MethodAppStore: + return MethodAppStoreConnect + case MethodAdHoc: + return MethodReleaseTesting + case MethodDevelopment: + return MethodDebugging + default: + return method + } +} + +// OnDemandResourcesAssetPacksBaseURLKey .... +const OnDemandResourcesAssetPacksBaseURLKey = "onDemandResourcesAssetPacksBaseURL" + +// TeamIDKey ... +const TeamIDKey = "teamID" + +// ThinningKey ... +const ThinningKey = "thinning" + +const ( + // ThinningNone ... + ThinningNone = "none" + // ThinningThinForAllVariants ... + ThinningThinForAllVariants = "thin-for-all-variants" + // ThinningDefault ... + ThinningDefault = ThinningNone +) + +// UploadBitcodeKey .... +const UploadBitcodeKey = "uploadBitcode" + +// UploadBitcodeDefault ... +const UploadBitcodeDefault = true + +// UploadSymbolsKey ... +const UploadSymbolsKey = "uploadSymbols" + +// UploadSymbolsDefault ... +const UploadSymbolsDefault = true + +const ( + manageAppVersionKey = "manageAppVersionAndBuildNumber" + manageAppVersionDefault = true +) + +// ProvisioningProfilesKey ... +const ProvisioningProfilesKey = "provisioningProfiles" + +// SigningCertificateKey ... +const SigningCertificateKey = "signingCertificate" + +// InstallerSigningCertificateKey ... +const InstallerSigningCertificateKey = "installerSigningCertificate" + +// SigningStyleKey ... +const SigningStyleKey = "signingStyle" + +// SigningStyle ... +type SigningStyle string + +// SigningStyle ... +const ( + SigningStyleManual SigningStyle = "manual" + SigningStyleAutomatic SigningStyle = "automatic" +) + +const DestinationKey = "destination" + +const TestFlightInternalTestingOnlyDefault = false +const TestFlightInternalTestingOnlyKey = "testFlightInternalTestingOnly" + +type Destination string + +// Destination ... +const ( + DestinationExport Destination = "export" + DestinationDefault Destination = DestinationExport +) diff --git a/exportoptions/properties_test.go b/exportoptions/properties_test.go new file mode 100644 index 00000000..f3b3ca95 --- /dev/null +++ b/exportoptions/properties_test.go @@ -0,0 +1,45 @@ +package exportoptions + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUpgradeToXcode15_3MethodName(t *testing.T) { + tests := []struct { + name string + method Method + want Method + }{ + { + method: "app-store", + want: "app-store-connect", + }, + { + method: "ad-hoc", + want: "release-testing", + }, + { + method: "development", + want: "debugging", + }, + { + method: "enterprise", + want: "enterprise", + }, + { + method: "developer-id", + want: "developer-id", + }, + { + method: "package", + want: "package", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, UpgradeToXcode15_3MethodName(tt.method)) + }) + } +} diff --git a/exportoptionsgenerator/exportoptionsgenerator.go b/exportoptionsgenerator/exportoptionsgenerator.go index c8536130..62dc0f14 100644 --- a/exportoptionsgenerator/exportoptionsgenerator.go +++ b/exportoptionsgenerator/exportoptionsgenerator.go @@ -6,9 +6,9 @@ import ( "github.com/bitrise-io/go-utils/sliceutil" "github.com/bitrise-io/go-utils/v2/log" - "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/export" + "github.com/bitrise-io/go-xcode/v2/exportoptions" "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/bitrise-io/go-xcode/v2/xcodeversion" ) diff --git a/exportoptionsgenerator/exportoptionsgenerator_test.go b/exportoptionsgenerator/exportoptionsgenerator_test.go index a9ec9d45..4beff16b 100644 --- a/exportoptionsgenerator/exportoptionsgenerator_test.go +++ b/exportoptionsgenerator/exportoptionsgenerator_test.go @@ -5,9 +5,9 @@ import ( "testing" "github.com/bitrise-io/go-utils/v2/log" - "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/exportoptions" "github.com/bitrise-io/go-xcode/v2/exportoptionsgenerator/mocks" "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/bitrise-io/go-xcode/v2/xcodeversion" diff --git a/metaparser/metaparser.go b/metaparser/metaparser.go index 205f1b3a..17c60440 100644 --- a/metaparser/metaparser.go +++ b/metaparser/metaparser.go @@ -5,7 +5,7 @@ import ( "github.com/bitrise-io/go-utils/v2/fileutil" "github.com/bitrise-io/go-utils/v2/log" - "github.com/bitrise-io/go-xcode/exportoptions" + "github.com/bitrise-io/go-xcode/v2/exportoptions" ) // ArtifactMetadata ... diff --git a/profileutil/info_model.go b/profileutil/info_model.go index cc1e1075..b8573fd1 100644 --- a/profileutil/info_model.go +++ b/profileutil/info_model.go @@ -8,12 +8,12 @@ import ( "strings" "time" - "howett.net/plist" + "github.com/bitrise-io/go-plist" "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/exportoptions" "github.com/fullsailor/pkcs7" ) diff --git a/profileutil/plist_data.go b/profileutil/plist_data.go index de9dafe1..eb35ee37 100644 --- a/profileutil/plist_data.go +++ b/profileutil/plist_data.go @@ -4,9 +4,9 @@ import ( "strings" "time" - "github.com/bitrise-io/go-xcode/exportoptions" + "github.com/bitrise-io/go-plist" "github.com/bitrise-io/go-xcode/plistutil" - "howett.net/plist" + "github.com/bitrise-io/go-xcode/v2/exportoptions" ) // PlistData ... diff --git a/profileutil/plist_data_test.go b/profileutil/plist_data_test.go index 7a0d08d9..672aa9f0 100644 --- a/profileutil/plist_data_test.go +++ b/profileutil/plist_data_test.go @@ -3,8 +3,8 @@ package profileutil import ( "testing" - "github.com/bitrise-io/go-xcode/exportoptions" "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/v2/exportoptions" "github.com/stretchr/testify/require" ) diff --git a/xcarchive/entitlements_test.go b/xcarchive/entitlements_test.go deleted file mode 100644 index e6de76a2..00000000 --- a/xcarchive/entitlements_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package xcarchive - -import ( - "path/filepath" - "testing" - - "github.com/bitrise-io/go-utils/pathutil" - - "github.com/bitrise-io/go-xcode/plistutil" - "github.com/stretchr/testify/assert" -) - -func TestGiveniOS_WhenAskingForEntitlements_ThenReadsItFromTheExecutable(t *testing.T) { - // Given - appPath := filepath.Join(sampleRepoPath(t), "archives/Fruta.xcarchive/Products/Applications/Fruta.app") - executable := executableRelativePath(appPath, "Info.plist", "") - - // When - entitlements, err := getEntitlements(appPath, executable) - - // Then - assert.NoError(t, err) - assert.Equal(t, iosEntitlements(), entitlements) -} - -func TestGivenMacos_WhenAskingForEntitlements_ThenReadsItFromTheExecutable(t *testing.T) { - // Given - appPath := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive/Products/Applications/Test.app") - executable := executableRelativePath(appPath, "Contents/Info.plist", "Contents/MacOS/") - - // When - entitlements, err := getEntitlements(appPath, executable) - - // Then - assert.NoError(t, err) - assert.Equal(t, macosEntitlements(), entitlements) -} - -func executableRelativePath(basePath, infoPlistRelativePath, executableFolderRelativePath string) string { - infoPlistPath := filepath.Join(basePath, infoPlistRelativePath) - exist, err := pathutil.IsPathExists(infoPlistPath) - if err != nil { - return "" - } - - if exist == false { - return "" - } - - plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) - if err != nil { - return "" - } - - return filepath.Join(executableFolderRelativePath, executableNameFromInfoPlist(plist)) -} - -func iosEntitlements() plistutil.PlistData { - return map[string]interface{}{ - "application-identifier": "72SA8V3WYL.io.bitrise.appcliptest", - "com.apple.developer.applesignin": []interface{}{"Default"}, - "com.apple.developer.icloud-container-identifiers": []interface{}{}, - "com.apple.developer.team-identifier": "72SA8V3WYL", - "com.apple.security.application-groups": []interface{}{"group.io.bitrise.appcliptest"}, - "get-task-allow": false, - } -} - -func macosEntitlements() plistutil.PlistData { - return map[string]interface{}{ - "com.apple.security.app-sandbox": true, - "com.apple.security.files.user-selected.read-only": true, - } -} diff --git a/xcarchive/entitlements.go b/xcarchive/utils.go similarity index 65% rename from xcarchive/entitlements.go rename to xcarchive/utils.go index 7908e5cc..987257c7 100644 --- a/xcarchive/entitlements.go +++ b/xcarchive/utils.go @@ -3,8 +3,10 @@ package xcarchive import ( "fmt" "path/filepath" + "strings" "github.com/bitrise-io/go-utils/command" + "github.com/bitrise-io/go-utils/pathutil" "github.com/bitrise-io/go-xcode/plistutil" ) @@ -44,3 +46,23 @@ func entitlementsFromExecutable(basePath, executableRelativePath string) (*plist return &plist, nil } + +func findDSYMs(archivePath string) ([]string, []string, error) { + dsymsDirPth := filepath.Join(archivePath, "dSYMs") + dsyms, err := pathutil.ListEntries(dsymsDirPth, pathutil.ExtensionFilter(".dsym", true)) + if err != nil { + return []string{}, []string{}, err + } + + appDSYMs := []string{} + frameworkDSYMs := []string{} + for _, dsym := range dsyms { + if strings.HasSuffix(dsym, ".app.dSYM") { + appDSYMs = append(appDSYMs, dsym) + } else { + frameworkDSYMs = append(frameworkDSYMs, dsym) + } + } + + return appDSYMs, frameworkDSYMs, nil +} diff --git a/xcarchive/xcarchive_test.go b/xcarchive/utils_test.go similarity index 64% rename from xcarchive/xcarchive_test.go rename to xcarchive/utils_test.go index 70b5d867..19cf88aa 100644 --- a/xcarchive/xcarchive_test.go +++ b/xcarchive/utils_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-xcode/plistutil" "github.com/stretchr/testify/assert" ) @@ -15,37 +16,66 @@ const ( DSYMSDirName = "dSYMs" ) -func TestIsMacOS(t *testing.T) { - tests := []struct { - name string - archPath string - want bool - wantErr bool - }{ - { - name: "macOS", - archPath: filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive"), - want: true, - wantErr: false, - }, - { - name: "iOS", - archPath: filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive"), - want: false, - wantErr: false, - }, +func TestGiveniOS_WhenAskingForEntitlements_ThenReadsItFromTheExecutable(t *testing.T) { + // Given + appPath := filepath.Join(sampleRepoPath(t), "archives/Fruta.xcarchive/Products/Applications/Fruta.app") + executable := executableRelativePath(appPath, "Info.plist", "") + + // When + entitlements, err := getEntitlements(appPath, executable) + + // Then + assert.NoError(t, err) + assert.Equal(t, iosEntitlements(), entitlements) +} + +func TestGivenMacos_WhenAskingForEntitlements_ThenReadsItFromTheExecutable(t *testing.T) { + // Given + appPath := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive/Products/Applications/Test.app") + executable := executableRelativePath(appPath, "Contents/Info.plist", "Contents/MacOS/") + + // When + entitlements, err := getEntitlements(appPath, executable) + + // Then + assert.NoError(t, err) + assert.Equal(t, macosEntitlements(), entitlements) +} + +func executableRelativePath(basePath, infoPlistRelativePath, executableFolderRelativePath string) string { + infoPlistPath := filepath.Join(basePath, infoPlistRelativePath) + exist, err := pathutil.IsPathExists(infoPlistPath) + if err != nil { + return "" } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := IsMacOS(tt.archPath) - if (err != nil) != tt.wantErr { - t.Errorf("IsMacOS() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("IsMacOS() = %v, want %v", got, tt.want) - } - }) + + if exist == false { + return "" + } + + plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + if err != nil { + return "" + } + + return filepath.Join(executableFolderRelativePath, executableNameFromInfoPlist(plist)) +} + +func iosEntitlements() plistutil.PlistData { + return map[string]interface{}{ + "application-identifier": "72SA8V3WYL.io.bitrise.appcliptest", + "com.apple.developer.applesignin": []interface{}{"Default"}, + "com.apple.developer.icloud-container-identifiers": []interface{}{}, + "com.apple.developer.team-identifier": "72SA8V3WYL", + "com.apple.security.application-groups": []interface{}{"group.io.bitrise.appcliptest"}, + "get-task-allow": false, + } +} + +func macosEntitlements() plistutil.PlistData { + return map[string]interface{}{ + "com.apple.security.app-sandbox": true, + "com.apple.security.files.user-selected.read-only": true, } } diff --git a/xcarchive/xcarchive.go b/xcarchive/xcarchive.go deleted file mode 100644 index 9565ceef..00000000 --- a/xcarchive/xcarchive.go +++ /dev/null @@ -1,88 +0,0 @@ -package xcarchive - -import ( - "path/filepath" - "strings" - - "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-utils/pathutil" - "github.com/bitrise-io/go-utils/ziputil" - "github.com/bitrise-io/go-xcode/plistutil" - "github.com/bitrise-io/go-xcode/utility" -) - -// IsMacOS try to find the Contents dir under the .app/. -// If its finds it the archive is MacOs. If it does not the archive is iOS. -func IsMacOS(archPath string) (bool, error) { - log.Debugf("Checking archive is MacOS or iOS") - infoPlistPath := filepath.Join(archPath, "Info.plist") - - plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) - if err != nil { - return false, err - } - - appProperties, found := plist.GetMapStringInterface("ApplicationProperties") - if !found { - return false, err - } - - applicationPath, found := appProperties.GetString("ApplicationPath") - if !found { - return false, err - } - - applicationPath = filepath.Join(archPath, "Products", applicationPath) - contentsPath := filepath.Join(applicationPath, "Contents") - - exist, err := pathutil.IsDirExists(contentsPath) - if err != nil { - return false, err - } - - return exist, nil -} - -// UnzipXcarchive ... -func UnzipXcarchive(xcarchivePth string) (string, error) { - tmpDir, err := pathutil.NormalizedOSTempDirPath("__xcarhive__") - if err != nil { - return "", err - } - - return tmpDir, ziputil.UnZip(xcarchivePth, tmpDir) -} - -// GetEmbeddedMobileProvisionPath ... -func GetEmbeddedMobileProvisionPath(xcarchivePth string) (string, error) { - return utility.FindFileInAppDir(getAppSubfolder(xcarchivePth), "embedded.mobileprovision") -} - -// GetEmbeddedInfoPlistPath ... -func GetEmbeddedInfoPlistPath(xcarchivePth string) (string, error) { - return utility.FindFileInAppDir(getAppSubfolder(xcarchivePth), "Info.plist") -} - -func getAppSubfolder(basepth string) string { - return filepath.Join(basepth, "Products", "Applications") -} - -func findDSYMs(archivePath string) ([]string, []string, error) { - dsymsDirPth := filepath.Join(archivePath, "dSYMs") - dsyms, err := pathutil.ListEntries(dsymsDirPth, pathutil.ExtensionFilter(".dsym", true)) - if err != nil { - return []string{}, []string{}, err - } - - appDSYMs := []string{} - frameworkDSYMs := []string{} - for _, dsym := range dsyms { - if strings.HasSuffix(dsym, ".app.dSYM") { - appDSYMs = append(appDSYMs, dsym) - } else { - frameworkDSYMs = append(frameworkDSYMs, dsym) - } - } - - return appDSYMs, frameworkDSYMs, nil -} From 72bf032bfc24910dda48f07b005466ccf7929b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krisztia=CC=81n=20Go=CC=88drei?= Date: Fri, 20 Jun 2025 14:43:53 +0200 Subject: [PATCH 5/7] Move plistutil to v2 and renamePlistData to MapData --- _integration_tests/zip/ipa_reader_test.go | 14 +- artifacts/ios_xcarchive_reader.go | 6 +- artifacts/ipa_reader.go | 6 +- artifacts/xcarchive_reader.go | 6 +- export/filter.go | 4 +- .../exportoptionsgenerator.go | 8 +- .../exportoptionsgenerator_test.go | 8 +- exportoptionsgenerator/targets.go | 8 +- plistutil/plistutil.go | 230 +++++++++++++++ plistutil/plistutil_test.go | 267 +++++++++++++++++ plistutil/plistutil_test_file_content.go | 276 ++++++++++++++++++ profileutil/capabilities.go | 4 +- profileutil/info_model.go | 8 +- profileutil/plist_data.go | 34 +-- profileutil/plist_data_test.go | 12 +- xcarchive/ios.go | 22 +- xcarchive/ios_test.go | 6 +- xcarchive/macos.go | 20 +- xcarchive/utils.go | 14 +- xcarchive/utils_test.go | 8 +- 20 files changed, 867 insertions(+), 94 deletions(-) create mode 100644 plistutil/plistutil.go create mode 100644 plistutil/plistutil_test.go create mode 100644 plistutil/plistutil_test_file_content.go diff --git a/_integration_tests/zip/ipa_reader_test.go b/_integration_tests/zip/ipa_reader_test.go index 2bd783d6..19f74356 100644 --- a/_integration_tests/zip/ipa_reader_test.go +++ b/_integration_tests/zip/ipa_reader_test.go @@ -6,10 +6,10 @@ import ( "testing" "github.com/bitrise-io/go-utils/v2/log" - "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/_integration_tests" "github.com/bitrise-io/go-xcode/v2/artifacts" internalzip "github.com/bitrise-io/go-xcode/v2/internal/zip" + "github.com/bitrise-io/go-xcode/v2/plistutil" "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/bitrise-io/go-xcode/v2/zip" "github.com/stretchr/testify/require" @@ -57,10 +57,10 @@ func Benchmark_ZipReaders(b *testing.B) { watchTestIPAPath := filepath.Join(sampleArtifactsDir, "ipas", "watch-test.ipa") for name, zipFunc := range map[string]readIPAFunc{ - "dittoReader": func() (plistutil.PlistData, *profileutil.ProvisioningProfileInfoModel) { + "dittoReader": func() (plistutil.MapData, *profileutil.ProvisioningProfileInfoModel) { return readIPAWithDittoZipReader(b, watchTestIPAPath) }, - "stdlibReader": func() (plistutil.PlistData, *profileutil.ProvisioningProfileInfoModel) { + "stdlibReader": func() (plistutil.MapData, *profileutil.ProvisioningProfileInfoModel) { return readIPAWithStdlibZipReader(b, watchTestIPAPath) }, } { @@ -70,9 +70,9 @@ func Benchmark_ZipReaders(b *testing.B) { } } -type readIPAFunc func() (plistutil.PlistData, *profileutil.ProvisioningProfileInfoModel) +type readIPAFunc func() (plistutil.MapData, *profileutil.ProvisioningProfileInfoModel) -func readIPAWithStdlibZipReader(t require.TestingT, archivePth string) (plistutil.PlistData, *profileutil.ProvisioningProfileInfoModel) { +func readIPAWithStdlibZipReader(t require.TestingT, archivePth string) (plistutil.MapData, *profileutil.ProvisioningProfileInfoModel) { r, err := internalzip.NewStdlibRead(archivePth, log.NewLogger()) require.NoError(t, err) defer func() { @@ -83,7 +83,7 @@ func readIPAWithStdlibZipReader(t require.TestingT, archivePth string) (plistuti return readIPA(t, r) } -func readIPAWithDittoZipReader(t require.TestingT, archivePth string) (plistutil.PlistData, *profileutil.ProvisioningProfileInfoModel) { +func readIPAWithDittoZipReader(t require.TestingT, archivePth string) (plistutil.MapData, *profileutil.ProvisioningProfileInfoModel) { r := internalzip.NewDittoReader(archivePth, log.NewLogger()) defer func() { err := r.Close() @@ -93,7 +93,7 @@ func readIPAWithDittoZipReader(t require.TestingT, archivePth string) (plistutil return readIPA(t, r) } -func readIPA(t require.TestingT, zipReader artifacts.ZipReadCloser) (plistutil.PlistData, *profileutil.ProvisioningProfileInfoModel) { +func readIPA(t require.TestingT, zipReader artifacts.ZipReadCloser) (plistutil.MapData, *profileutil.ProvisioningProfileInfoModel) { ipaReader := artifacts.NewIPAReader(zipReader) plist, err := ipaReader.AppInfoPlist() require.NoError(t, err) diff --git a/artifacts/ios_xcarchive_reader.go b/artifacts/ios_xcarchive_reader.go index 96b4717c..364dacd1 100644 --- a/artifacts/ios_xcarchive_reader.go +++ b/artifacts/ios_xcarchive_reader.go @@ -1,7 +1,7 @@ package artifacts import ( - "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/v2/plistutil" ) // IOSXCArchiveReader ... @@ -15,11 +15,11 @@ func NewIOSXCArchiveReader(reader ZipReadCloser) IOSXCArchiveReader { } // AppInfoPlist ... -func (reader IOSXCArchiveReader) AppInfoPlist() (plistutil.PlistData, error) { +func (reader IOSXCArchiveReader) AppInfoPlist() (plistutil.MapData, error) { b, err := reader.zipReader.ReadFile("*.xcarchive/Products/Applications/*.app/Info.plist") if err != nil { return nil, err } - return plistutil.NewPlistDataFromContent(string(b)) + return plistutil.NewMapDataFromPlistContent(string(b)) } diff --git a/artifacts/ipa_reader.go b/artifacts/ipa_reader.go index 6493f02f..00684404 100644 --- a/artifacts/ipa_reader.go +++ b/artifacts/ipa_reader.go @@ -3,7 +3,7 @@ package artifacts import ( "fmt" - "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/v2/plistutil" "github.com/bitrise-io/go-xcode/v2/profileutil" ) @@ -38,11 +38,11 @@ func (reader IPAReader) ProvisioningProfileInfo() (*profileutil.ProvisioningProf } // AppInfoPlist ... -func (reader IPAReader) AppInfoPlist() (plistutil.PlistData, error) { +func (reader IPAReader) AppInfoPlist() (plistutil.MapData, error) { b, err := reader.zipReader.ReadFile("Payload/*.app/Info.plist") if err != nil { return nil, err } - return plistutil.NewPlistDataFromContent(string(b)) + return plistutil.NewMapDataFromPlistContent(string(b)) } diff --git a/artifacts/xcarchive_reader.go b/artifacts/xcarchive_reader.go index 02368165..bd1a61b5 100644 --- a/artifacts/xcarchive_reader.go +++ b/artifacts/xcarchive_reader.go @@ -1,7 +1,7 @@ package artifacts import ( - "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/v2/plistutil" ) // XCArchiveReader ... @@ -15,13 +15,13 @@ func NewXCArchiveReader(reader ZipReadCloser) XCArchiveReader { } // InfoPlist ... -func (reader XCArchiveReader) InfoPlist() (plistutil.PlistData, error) { +func (reader XCArchiveReader) InfoPlist() (plistutil.MapData, error) { b, err := reader.zipReader.ReadFile("*.xcarchive/Info.plist") if err != nil { return nil, err } - return plistutil.NewPlistDataFromContent(string(b)) + return plistutil.NewMapDataFromPlistContent(string(b)) } // IsMacOS ... diff --git a/export/filter.go b/export/filter.go index 53b19632..7410acde 100644 --- a/export/filter.go +++ b/export/filter.go @@ -2,8 +2,8 @@ package export import ( "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/exportoptions" + "github.com/bitrise-io/go-xcode/v2/plistutil" "github.com/bitrise-io/go-xcode/v2/profileutil" ) @@ -33,7 +33,7 @@ func FilterSelectableCodeSignGroups(groups []SelectableCodeSignGroup, filterFunc } // CreateEntitlementsSelectableCodeSignGroupFilter ... -func CreateEntitlementsSelectableCodeSignGroupFilter(bundleIDEntitlementsMap map[string]plistutil.PlistData) SelectableCodeSignGroupFilter { +func CreateEntitlementsSelectableCodeSignGroupFilter(bundleIDEntitlementsMap map[string]plistutil.MapData) SelectableCodeSignGroupFilter { return func(group *SelectableCodeSignGroup) bool { log.Debugf("Entitlements filter - removes profile if has missing capabilities") diff --git a/exportoptionsgenerator/exportoptionsgenerator.go b/exportoptionsgenerator/exportoptionsgenerator.go index 62dc0f14..ebb412ec 100644 --- a/exportoptionsgenerator/exportoptionsgenerator.go +++ b/exportoptionsgenerator/exportoptionsgenerator.go @@ -6,9 +6,9 @@ import ( "github.com/bitrise-io/go-utils/sliceutil" "github.com/bitrise-io/go-utils/v2/log" - "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/export" "github.com/bitrise-io/go-xcode/v2/exportoptions" + "github.com/bitrise-io/go-xcode/v2/plistutil" "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/bitrise-io/go-xcode/v2/xcodeversion" ) @@ -127,7 +127,7 @@ func (g ExportOptionsGenerator) GenerateApplicationExportOptions( // determineCodesignGroup finds the best codesign group (certificate + profiles) // based on the installed Provisioning Profiles and Codesign Certificates. -func (g ExportOptionsGenerator) determineCodesignGroup(bundleIDEntitlementsMap map[string]plistutil.PlistData, exportMethod exportoptions.Method, teamID string, xcodeManaged bool) (*export.IosCodeSignGroup, error) { +func (g ExportOptionsGenerator) determineCodesignGroup(bundleIDEntitlementsMap map[string]plistutil.MapData, exportMethod exportoptions.Method, teamID string, xcodeManaged bool) (*export.IosCodeSignGroup, error) { fmt.Println() g.logger.Printf("Target Bundle ID - Entitlements map") var bundleIDs []string @@ -272,7 +272,7 @@ func (g ExportOptionsGenerator) determineCodesignGroup(bundleIDEntitlementsMap m } // determineIcloudContainerEnvironment calculates the value of iCloudContainerEnvironment. -func determineIcloudContainerEnvironment(desiredIcloudContainerEnvironment string, bundleIDEntitlementsMap map[string]plistutil.PlistData, exportMethod exportoptions.Method, xcodeMajorVersion int64) (string, error) { +func determineIcloudContainerEnvironment(desiredIcloudContainerEnvironment string, bundleIDEntitlementsMap map[string]plistutil.MapData, exportMethod exportoptions.Method, xcodeMajorVersion int64) (string, error) { // iCloudContainerEnvironment: If the app is using CloudKit, this configures the "com.apple.developer.icloud-container-environment" entitlement. // Available options vary depending on the type of provisioning profile used, but may include: Development and Production. usesCloudKit := projectUsesCloudKit(bundleIDEntitlementsMap) @@ -298,7 +298,7 @@ func determineIcloudContainerEnvironment(desiredIcloudContainerEnvironment strin } // projectUsesCloudKit determines whether the project uses any CloudKit capability or not. -func projectUsesCloudKit(bundleIDEntitlementsMap map[string]plistutil.PlistData) bool { +func projectUsesCloudKit(bundleIDEntitlementsMap map[string]plistutil.MapData) bool { fmt.Printf("Checking if project uses CloudKit") for _, entitlements := range bundleIDEntitlementsMap { diff --git a/exportoptionsgenerator/exportoptionsgenerator_test.go b/exportoptionsgenerator/exportoptionsgenerator_test.go index 4beff16b..ac7266b2 100644 --- a/exportoptionsgenerator/exportoptionsgenerator_test.go +++ b/exportoptionsgenerator/exportoptionsgenerator_test.go @@ -5,10 +5,10 @@ import ( "testing" "github.com/bitrise-io/go-utils/v2/log" - "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/bitrise-io/go-xcode/v2/exportoptions" "github.com/bitrise-io/go-xcode/v2/exportoptionsgenerator/mocks" + "github.com/bitrise-io/go-xcode/v2/plistutil" "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/bitrise-io/go-xcode/v2/xcodeversion" "github.com/stretchr/testify/require" @@ -287,7 +287,7 @@ func TestExportOptionsGenerator_GenerateApplicationExportOptions_ForAutomaticSig exportProduct: ExportProductApp, archiveInfo: ArchiveInfo{ AppBundleID: bundleID, - EntitlementsByBundleID: map[string]plistutil.PlistData{ + EntitlementsByBundleID: map[string]plistutil.MapData{ bundleID: {"com.apple.developer.icloud-services": []string{"CloudKit"}}, }, }, @@ -447,7 +447,7 @@ func TestExportOptionsGenerator_GenerateApplicationExportOptions(t *testing.T) { archiveInfo := ArchiveInfo{ AppBundleID: bundleID, - EntitlementsByBundleID: map[string]plistutil.PlistData{ + EntitlementsByBundleID: map[string]plistutil.MapData{ bundleID: {"com.apple.developer.icloud-services": []string{"CloudKit"}}, bundleIDClip: nil, }, @@ -540,7 +540,7 @@ func TestExportOptionsGenerator_GenerateApplicationExportOptions_WhenNoProfileFo archiveInfo := ArchiveInfo{ AppBundleID: bundleID, - EntitlementsByBundleID: map[string]plistutil.PlistData{ + EntitlementsByBundleID: map[string]plistutil.MapData{ bundleID: cloudKitEntitlement, bundleIDClip: nil, }, diff --git a/exportoptionsgenerator/targets.go b/exportoptionsgenerator/targets.go index d7d014ee..4ee516fb 100644 --- a/exportoptionsgenerator/targets.go +++ b/exportoptionsgenerator/targets.go @@ -3,7 +3,7 @@ package exportoptionsgenerator import ( "fmt" - "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/v2/plistutil" "github.com/bitrise-io/go-xcode/xcodeproject/serialized" "github.com/bitrise-io/go-xcode/xcodeproject/xcodeproj" "github.com/bitrise-io/go-xcode/xcodeproject/xcscheme" @@ -13,7 +13,7 @@ import ( type ArchiveInfo struct { AppBundleID string AppClipBundleID string - EntitlementsByBundleID map[string]plistutil.PlistData + EntitlementsByBundleID map[string]plistutil.MapData } // ReadArchiveInfoFromXcodeproject reads the Bundle ID for the given scheme and configuration. @@ -28,7 +28,7 @@ func ReadArchiveInfoFromXcodeproject(xcodeProj *xcodeproj.XcodeProj, scheme *xcs mainTargetBundleID := "" appClipBundleID := "" - entitlementsByBundleID := map[string]plistutil.PlistData{} + entitlementsByBundleID := map[string]plistutil.MapData{} for i, target := range targets { bundleID, err := xcodeProj.TargetBundleID(target.Name, configuration) if err != nil { @@ -40,7 +40,7 @@ func ReadArchiveInfoFromXcodeproject(xcodeProj *xcodeproj.XcodeProj, scheme *xcs return ArchiveInfo{}, fmt.Errorf("failed to get target (%s) bundle id: %s", target.Name, err) } - entitlementsByBundleID[bundleID] = plistutil.PlistData(entitlements) + entitlementsByBundleID[bundleID] = plistutil.MapData(entitlements) if target.IsAppClipProduct() { appClipBundleID = bundleID diff --git a/plistutil/plistutil.go b/plistutil/plistutil.go new file mode 100644 index 00000000..66607e74 --- /dev/null +++ b/plistutil/plistutil.go @@ -0,0 +1,230 @@ +package plistutil + +import ( + "errors" + "time" + + "github.com/bitrise-io/go-plist" + "github.com/bitrise-io/go-utils/fileutil" +) + +// MapData ... +type MapData map[string]interface{} + +// NewMapDataFromPlistContent ... +func NewMapDataFromPlistContent(plistContent string) (MapData, error) { + var data MapData + if _, err := plist.Unmarshal([]byte(plistContent), &data); err != nil { + return MapData{}, err + } + return data, nil +} + +// NewMapDataFromPlistFile ... +func NewMapDataFromPlistFile(plistPth string) (MapData, error) { + content, err := fileutil.ReadStringFromFile(plistPth) + if err != nil { + return MapData{}, err + } + return NewMapDataFromPlistContent(content) +} + +// GetString ... +func (data MapData) GetString(forKey string) (string, bool) { + value, ok := data[forKey] + if !ok { + return "", false + } + + casted, ok := value.(string) + if !ok { + return "", false + } + + return casted, true +} + +// GetUInt64 ... +func (data MapData) GetUInt64(forKey string) (uint64, bool) { + value, ok := data[forKey] + if !ok { + return 0, false + } + + casted, ok := value.(uint64) + if !ok { + return 0, false + } + return casted, true +} + +// GetFloat64 ... +func (data MapData) GetFloat64(forKey string) (float64, bool) { + value, ok := data[forKey] + if !ok { + return 0, false + } + + casted, ok := value.(float64) + if !ok { + return 0, false + } + return casted, true +} + +// GetBool ... +func (data MapData) GetBool(forKey string) (bool, bool) { + value, ok := data[forKey] + if !ok { + return false, false + } + + casted, ok := value.(bool) + if !ok { + return false, false + } + + return casted, true +} + +// GetTime ... +func (data MapData) GetTime(forKey string) (time.Time, bool) { + value, ok := data[forKey] + if !ok { + return time.Time{}, false + } + + casted, ok := value.(time.Time) + if !ok { + return time.Time{}, false + } + return casted, true +} + +// GetUInt64Array ... +func (data MapData) GetUInt64Array(forKey string) ([]uint64, bool) { + value, ok := data[forKey] + if !ok { + return nil, false + } + + if casted, ok := value.([]uint64); ok { + return casted, true + } + + casted, ok := value.([]interface{}) + if !ok { + return nil, false + } + + array := []uint64{} + for _, v := range casted { + casted, ok := v.(uint64) + if !ok { + return nil, false + } + + array = append(array, casted) + } + return array, true +} + +// GetStringArray ... +func (data MapData) GetStringArray(forKey string) ([]string, bool) { + value, ok := data[forKey] + if !ok { + return nil, false + } + + if casted, ok := value.([]string); ok { + return casted, true + } + + casted, ok := value.([]interface{}) + if !ok { + return nil, false + } + + array := []string{} + for _, v := range casted { + casted, ok := v.(string) + if !ok { + return nil, false + } + + array = append(array, casted) + } + return array, true +} + +// GetByteArrayArray ... +func (data MapData) GetByteArrayArray(forKey string) ([][]byte, bool) { + value, ok := data[forKey] + if !ok { + return nil, false + } + + if casted, ok := value.([][]byte); ok { + return casted, true + } + + casted, ok := value.([]interface{}) + if !ok { + return nil, false + } + + array := [][]byte{} + for _, v := range casted { + casted, ok := v.([]byte) + if !ok { + return nil, false + } + + array = append(array, casted) + } + return array, true +} + +// GetMapStringInterface ... +func (data MapData) GetMapStringInterface(forKey string) (MapData, bool) { + value, ok := data[forKey] + if !ok { + return nil, false + } + + if casted, ok := value.(map[string]interface{}); ok { + return casted, true + } + return nil, false +} + +func castToMapStringInterfaceArray(obj interface{}) ([]MapData, error) { + array, ok := obj.([]interface{}) + if !ok { + return nil, errors.New("failed to cast to []interface{}") + } + + var casted []MapData + for _, item := range array { + mapStringInterface, ok := item.(map[string]interface{}) + if !ok { + return nil, errors.New("failed to cast to map[string]interface{}") + } + casted = append(casted, mapStringInterface) + } + + return casted, nil +} + +// GetMapStringInterfaceArray ... +func (data MapData) GetMapStringInterfaceArray(forKey string) ([]MapData, bool) { + value, ok := data[forKey] + if !ok { + return nil, false + } + mapStringInterfaceArray, err := castToMapStringInterfaceArray(value) + if err != nil { + return nil, false + } + return mapStringInterfaceArray, true +} diff --git a/plistutil/plistutil_test.go b/plistutil/plistutil_test.go new file mode 100644 index 00000000..85d19a9d --- /dev/null +++ b/plistutil/plistutil_test.go @@ -0,0 +1,267 @@ +package plistutil + +import ( + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestAnalyzeInfoPlist(t *testing.T) { + infoPlistData, err := NewMapDataFromPlistContent(infoPlistContent) + require.NoError(t, err) + + appTitle, ok := infoPlistData.GetString("CFBundleName") + require.Equal(t, true, ok) + require.Equal(t, "ios-simple-objc", appTitle) + + bundleID, _ := infoPlistData.GetString("CFBundleIdentifier") + require.Equal(t, true, ok) + require.Equal(t, "Bitrise.ios-simple-objc", bundleID) + + version, ok := infoPlistData.GetString("CFBundleShortVersionString") + require.Equal(t, true, ok) + require.Equal(t, "1.0", version) + + buildNumber, ok := infoPlistData.GetString("CFBundleVersion") + require.Equal(t, true, ok) + require.Equal(t, "1", buildNumber) + + minOSVersion, ok := infoPlistData.GetString("MinimumOSVersion") + require.Equal(t, true, ok) + require.Equal(t, "8.1", minOSVersion) + + deviceFamilyList, ok := infoPlistData.GetUInt64Array("UIDeviceFamily") + require.Equal(t, true, ok) + require.Equal(t, 2, len(deviceFamilyList)) + require.Equal(t, uint64(1), deviceFamilyList[0]) + require.Equal(t, uint64(2), deviceFamilyList[1]) +} + +func TestAnalyzeEmbeddedProfile(t *testing.T) { + profileData, err := NewMapDataFromPlistContent(appStoreProfileContent) + require.NoError(t, err) + + creationDate, ok := profileData.GetTime("CreationDate") + require.Equal(t, true, ok) + expectedCreationDate, err := time.Parse("2006-01-02T15:04:05Z", "2016-09-22T11:29:12Z") + require.NoError(t, err) + require.Equal(t, true, creationDate.Equal(expectedCreationDate)) + + expirationDate, ok := profileData.GetTime("ExpirationDate") + require.Equal(t, true, ok) + expectedExpirationDate, err := time.Parse("2006-01-02T15:04:05Z", "2017-09-21T13:20:06Z") + require.NoError(t, err) + require.Equal(t, true, expirationDate.Equal(expectedExpirationDate)) + + deviceUDIDList, ok := profileData.GetStringArray("ProvisionedDevices") + require.Equal(t, false, ok) + require.Equal(t, 0, len(deviceUDIDList)) + + teamName, ok := profileData.GetString("TeamName") + require.Equal(t, true, ok) + require.Equal(t, "Some Dude", teamName) + + profileName, ok := profileData.GetString("Name") + require.Equal(t, true, ok) + require.Equal(t, "Bitrise Test App Store", profileName) + + provisionsAlldevices, ok := profileData.GetBool("ProvisionsAllDevices") + require.Equal(t, false, ok) + require.Equal(t, false, provisionsAlldevices) +} + +func TestGetBool(t *testing.T) { + profileData, err := NewMapDataFromPlistContent(enterpriseProfileContent) + require.NoError(t, err) + + allDevices, ok := profileData.GetBool("ProvisionsAllDevices") + require.Equal(t, true, ok) + require.Equal(t, true, allDevices) +} + +func TestGetTime(t *testing.T) { + profileData, err := NewMapDataFromPlistContent(developmentProfileContent) + require.NoError(t, err) + + expire, ok := profileData.GetTime("ExpirationDate") + require.Equal(t, true, ok) + + // 2017-09-22T11:28:46Z + desiredExpire, err := time.Parse("2006-01-02T15:04:05Z", "2017-09-22T11:28:46Z") + require.NoError(t, err) + require.Equal(t, true, expire.Equal(desiredExpire)) +} + +func TestGetInt(t *testing.T) { + profileData, err := NewMapDataFromPlistContent(developmentProfileContent) + require.NoError(t, err) + + version, ok := profileData.GetUInt64("Version") + require.Equal(t, true, ok) + require.Equal(t, uint64(1), version) +} + +func TestGetStringArray(t *testing.T) { + profileData, err := NewMapDataFromPlistContent(developmentProfileContent) + require.NoError(t, err) + + devices, ok := profileData.GetStringArray("ProvisionedDevices") + require.Equal(t, true, ok) + require.Equal(t, 1, len(devices)) + require.Equal(t, "b138", devices[0]) +} + +func TestGetMapStringInterface(t *testing.T) { + profileData, err := NewMapDataFromPlistContent(developmentProfileContent) + require.NoError(t, err) + + entitlements, ok := profileData.GetMapStringInterface("Entitlements") + require.Equal(t, true, ok) + + teamID, ok := entitlements.GetString("com.apple.developer.team-identifier") + require.Equal(t, true, ok) + require.Equal(t, "9NS4", teamID) +} + +func TestPlistData_GetMapStringInterfaceArray(t *testing.T) { + testSummariesData, err := NewMapDataFromPlistContent(paritalTestSummariesContent) + if err != nil { + t.Errorf("NewMapDataFromPlistContent(), got: %v, want: %v", err, nil) + } + const key = "Key" + + type args struct { + forKey string + } + tests := []struct { + name string + data MapData + args args + want []MapData + want1 bool + }{ + { + name: "Test ok case", + data: MapData{key: []interface{}{ + map[string]interface{}{"k1": "v1", "k2": "v2"}, + map[string]interface{}{"k3": "v3"}, + }}, + args: args{key}, + want: []MapData{ + map[string]interface{}{"k1": "v1", "k2": "v2"}, + map[string]interface{}{"k3": "v3"}, + }, + want1: true, + }, + { + name: "Test key not found", + data: MapData{"otherKey": []MapData{}}, + args: args{key}, + want: nil, + want1: false, + }, + { + name: "Test failed to cast to interface{}", + data: MapData{key: []MapData{ + map[string]interface{}{"k1": "v1", "k2": "v2"}, + map[string]interface{}{"k3": "v3"}, + }}, + args: args{key}, + want: nil, + want1: false, + }, + { + name: "Failed to cast array element to map[string]interface{}", + data: MapData{key: []interface{}{ + map[string]string{"k1": "v1", "k2": "v2"}, + map[string]string{"k3": "v3"}, + }}, + args: args{key}, + want: nil, + want1: false, + }, + { + name: "Intefration test with real plist data", + data: testSummariesData, + args: args{"Subtests"}, + want: []MapData{ + map[string]interface{}{"TestIdentifier": "ios_simple_objcTests/testExample", "TestStatus": "Success"}, + map[string]interface{}{"TestIdentifier": "ios_simple_objcTests/testExample2", "TestStatus": "Success"}, + }, + want1: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := tt.data.GetMapStringInterfaceArray(tt.args.forKey) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MapData.GetMapStringInterfaceArray() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("MapData.GetMapStringInterfaceArray() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestPlistData_GetFloat64(t *testing.T) { + testSummariesData, err := NewMapDataFromPlistContent(paritalTestSummariesContent) + if err != nil { + t.Errorf("NewMapDataFromPlistContent(), got: %v, want: %v", err, nil) + } + const key = "Duration" + const value = 0.00072991847991943359 + + type args struct { + forKey string + } + tests := []struct { + name string + data MapData + args args + want float64 + want1 bool + }{ + { + name: "Read float, ok", + data: map[string]interface{}{key: value}, + args: args{key}, + want: value, + want1: true, + }, + { + name: "Key not found", + data: map[string]interface{}{"otherKey": value}, + args: args{key}, + want: 0, + want1: false, + }, + { + name: "Read int value, fail", + data: map[string]interface{}{key: 23}, + args: args{key}, + want: 0, + want1: false, + }, + { + name: "Integration test with real plist data", + data: testSummariesData, + args: args{"Duration"}, + want: 0.34774100780487061, + want1: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := tt.data.GetFloat64(tt.args.forKey) + if got != tt.want { + t.Errorf("MapData.GetFloat64() got = %v, want %v", got, tt.want) + } + if got1 != tt.want1 { + t.Errorf("MapData.GetFloat64() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} diff --git a/plistutil/plistutil_test_file_content.go b/plistutil/plistutil_test_file_content.go new file mode 100644 index 00000000..ae6d45a5 --- /dev/null +++ b/plistutil/plistutil_test_file_content.go @@ -0,0 +1,276 @@ +package plistutil + +const infoPlistContent = ` + + + + CFBundleName + ios-simple-objc + DTXcode + 0832 + DTSDKName + iphoneos10.3 + UILaunchStoryboardName + LaunchScreen + DTSDKBuild + 14E269 + CFBundleDevelopmentRegion + en + CFBundleVersion + 1 + BuildMachineOSBuild + 16F73 + DTPlatformName + iphoneos + CFBundlePackageType + APPL + UIMainStoryboardFile + Main + CFBundleSupportedPlatforms + + iPhoneOS + + CFBundleShortVersionString + 1.0 + CFBundleInfoDictionaryVersion + 6.0 + UIRequiredDeviceCapabilities + + armv7 + + CFBundleExecutable + ios-simple-objc + DTCompiler + com.apple.compilers.llvm.clang.1_0 + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CFBundleIdentifier + Bitrise.ios-simple-objc + MinimumOSVersion + 8.1 + DTXcodeBuild + 8E2002 + DTPlatformVersion + 10.3 + LSRequiresIPhoneOS + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CFBundleSignature + ???? + UIDeviceFamily + + 1 + 2 + + DTPlatformBuild + 14E269 + + +` + +const developmentProfileContent = ` + + + + AppIDName + Bitrise Test + ApplicationIdentifierPrefix + + 9NS4 + + CreationDate + 2016-09-22T11:28:46Z + Platform + + iOS + + DeveloperCertificates + + + + Entitlements + + keychain-access-groups + + 9NS4.* + + get-task-allow + + application-identifier + 9NS4.* + com.apple.developer.team-identifier + 9NS4 + + ExpirationDate + 2017-09-22T11:28:46Z + Name + Bitrise Test Development + ProvisionedDevices + + b138 + + TeamIdentifier + + 9NS4 + + TeamName + Some Dude + TimeToLive + 365 + UUID + 4b617a5f + Version + 1 +` + +const appStoreProfileContent = ` + + + + AppIDName + Bitrise Test + ApplicationIdentifierPrefix + + 9NS4 + + CreationDate + 2016-09-22T11:29:12Z + Platform + + iOS + + DeveloperCertificates + + + + Entitlements + + keychain-access-groups + + 9NS4.* + + get-task-allow + + application-identifier + 9NS4.* + com.apple.developer.team-identifier + 9NS4 + beta-reports-active + + + ExpirationDate + 2017-09-21T13:20:06Z + Name + Bitrise Test App Store + TeamIdentifier + + 9NS4 + + TeamName + Some Dude + TimeToLive + 364 + UUID + a60668dd + Version + 1 +` + +const enterpriseProfileContent = ` + + + + AppIDName + Bitrise Test + ApplicationIdentifierPrefix + + PF3BP78LQ8 + + CreationDate + 2015-10-05T13:32:46Z + Platform + + iOS + + DeveloperCertificates + + + + Entitlements + + keychain-access-groups + + PF3BP78LQ8.* + + get-task-allow + + application-identifier + 9NS4.* + com.apple.developer.team-identifier + 9NS4 + + ExpirationDate + 2016-10-04T13:32:46Z + Name + Bitrise Test Enterprise + ProvisionsAllDevices + + TeamIdentifier + + PF3BP78LQ8 + + TeamName + Some Dude + TimeToLive + 365 + UUID + 8d6caa15 + Version + 1 +` + +const paritalTestSummariesContent = ` + + + + Duration + 0.34774100780487061 + Subtests + + + TestIdentifier + ios_simple_objcTests/testExample + TestStatus + Success + + + TestIdentifier + ios_simple_objcTests/testExample2 + TestStatus + Success + + + TestIdentifier + ios_simple_objcTests + TestName + ios_simple_objcTests + TestObjectClass + IDESchemeActionTestSummaryGroup + +TestIdentifier +ios-simple-objcTests.xctest +TestName +ios-simple-objcTests.xctest +TestObjectClass +IDESchemeActionTestSummaryGroup +` diff --git a/profileutil/capabilities.go b/profileutil/capabilities.go index 3e5b850b..fb076f4b 100644 --- a/profileutil/capabilities.go +++ b/profileutil/capabilities.go @@ -2,11 +2,11 @@ package profileutil import ( "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/v2/plistutil" ) // MatchTargetAndProfileEntitlements ... -func MatchTargetAndProfileEntitlements(targetEntitlements plistutil.PlistData, profileEntitlements plistutil.PlistData, profileType ProfileType) []string { +func MatchTargetAndProfileEntitlements(targetEntitlements plistutil.MapData, profileEntitlements plistutil.MapData, profileType ProfileType) []string { missingEntitlements := []string{} for key := range targetEntitlements { diff --git a/profileutil/info_model.go b/profileutil/info_model.go index b8573fd1..ac77adf3 100644 --- a/profileutil/info_model.go +++ b/profileutil/info_model.go @@ -11,9 +11,9 @@ import ( "github.com/bitrise-io/go-plist" "github.com/bitrise-io/go-utils/log" - "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/bitrise-io/go-xcode/v2/exportoptions" + "github.com/bitrise-io/go-xcode/v2/plistutil" "github.com/fullsailor/pkcs7" ) @@ -29,12 +29,12 @@ type ProvisioningProfileInfoModel struct { DeveloperCertificates []certificateutil.CertificateInfoModel CreationDate time.Time ExpirationDate time.Time - Entitlements plistutil.PlistData + Entitlements plistutil.MapData ProvisionsAllDevices bool Type ProfileType } -func collectCapabilitesPrintableInfo(entitlements plistutil.PlistData) map[string]interface{} { +func collectCapabilitesPrintableInfo(entitlements plistutil.MapData) map[string]interface{} { capabilities := map[string]interface{}{} for key, value := range entitlements { @@ -140,7 +140,7 @@ func (info ProvisioningProfileInfoModel) HasInstalledCertificate(installedCertif // NewProvisioningProfileInfo ... func NewProvisioningProfileInfo(provisioningProfile pkcs7.PKCS7) (ProvisioningProfileInfoModel, error) { - var data plistutil.PlistData + var data plistutil.MapData if _, err := plist.Unmarshal(provisioningProfile.Content, &data); err != nil { return ProvisioningProfileInfoModel{}, err } diff --git a/profileutil/plist_data.go b/profileutil/plist_data.go index eb35ee37..cb3b22aa 100644 --- a/profileutil/plist_data.go +++ b/profileutil/plist_data.go @@ -5,12 +5,12 @@ import ( "time" "github.com/bitrise-io/go-plist" - "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/exportoptions" + "github.com/bitrise-io/go-xcode/v2/plistutil" ) // PlistData ... -type PlistData plistutil.PlistData +type PlistData plistutil.MapData // NewPlistDataFromFile ... func NewPlistDataFromFile(provisioningProfilePth string) (PlistData, error) { @@ -19,7 +19,7 @@ func NewPlistDataFromFile(provisioningProfilePth string) (PlistData, error) { return PlistData{}, err } - var plistData plistutil.PlistData + var plistData plistutil.MapData if _, err := plist.Unmarshal(provisioningProfilePKCS7.Content, &plistData); err != nil { return PlistData{}, err } @@ -29,21 +29,21 @@ func NewPlistDataFromFile(provisioningProfilePth string) (PlistData, error) { // GetUUID ... func (profile PlistData) GetUUID() string { - data := plistutil.PlistData(profile) + data := plistutil.MapData(profile) uuid, _ := data.GetString("UUID") return uuid } // GetName ... func (profile PlistData) GetName() string { - data := plistutil.PlistData(profile) + data := plistutil.MapData(profile) uuid, _ := data.GetString("Name") return uuid } // GetApplicationIdentifier ... func (profile PlistData) GetApplicationIdentifier() string { - data := plistutil.PlistData(profile) + data := plistutil.MapData(profile) entitlements, ok := data.GetMapStringInterface("Entitlements") if !ok { return "" @@ -63,7 +63,7 @@ func (profile PlistData) GetApplicationIdentifier() string { func (profile PlistData) GetBundleIdentifier() string { applicationID := profile.GetApplicationIdentifier() - plistData := plistutil.PlistData(profile) + plistData := plistutil.MapData(profile) prefixes, found := plistData.GetStringArray("ApplicationIdentifierPrefix") if found { for _, prefix := range prefixes { @@ -77,7 +77,7 @@ func (profile PlistData) GetBundleIdentifier() string { // GetExportMethod ... func (profile PlistData) GetExportMethod() exportoptions.Method { - data := plistutil.PlistData(profile) + data := plistutil.MapData(profile) entitlements, _ := data.GetMapStringInterface("Entitlements") platform, _ := data.GetStringArray("Platform") @@ -111,15 +111,15 @@ func (profile PlistData) GetExportMethod() exportoptions.Method { } // GetEntitlements ... -func (profile PlistData) GetEntitlements() plistutil.PlistData { - data := plistutil.PlistData(profile) +func (profile PlistData) GetEntitlements() plistutil.MapData { + data := plistutil.MapData(profile) entitlements, _ := data.GetMapStringInterface("Entitlements") return entitlements } // GetTeamID ... func (profile PlistData) GetTeamID() string { - data := plistutil.PlistData(profile) + data := plistutil.MapData(profile) entitlements, ok := data.GetMapStringInterface("Entitlements") if ok { teamID, _ := entitlements.GetString("com.apple.developer.team-identifier") @@ -130,42 +130,42 @@ func (profile PlistData) GetTeamID() string { // GetExpirationDate ... func (profile PlistData) GetExpirationDate() time.Time { - data := plistutil.PlistData(profile) + data := plistutil.MapData(profile) expiry, _ := data.GetTime("ExpirationDate") return expiry } // GetProvisionedDevices ... func (profile PlistData) GetProvisionedDevices() []string { - data := plistutil.PlistData(profile) + data := plistutil.MapData(profile) devices, _ := data.GetStringArray("ProvisionedDevices") return devices } // GetDeveloperCertificates ... func (profile PlistData) GetDeveloperCertificates() [][]byte { - data := plistutil.PlistData(profile) + data := plistutil.MapData(profile) developerCertificates, _ := data.GetByteArrayArray("DeveloperCertificates") return developerCertificates } // GetTeamName ... func (profile PlistData) GetTeamName() string { - data := plistutil.PlistData(profile) + data := plistutil.MapData(profile) teamName, _ := data.GetString("TeamName") return teamName } // GetCreationDate ... func (profile PlistData) GetCreationDate() time.Time { - data := plistutil.PlistData(profile) + data := plistutil.MapData(profile) creationDate, _ := data.GetTime("CreationDate") return creationDate } // GetProvisionsAllDevices ... func (profile PlistData) GetProvisionsAllDevices() bool { - data := plistutil.PlistData(profile) + data := plistutil.MapData(profile) provisionsAlldevices, _ := data.GetBool("ProvisionsAllDevices") return provisionsAlldevices } diff --git a/profileutil/plist_data_test.go b/profileutil/plist_data_test.go index 672aa9f0..e1971738 100644 --- a/profileutil/plist_data_test.go +++ b/profileutil/plist_data_test.go @@ -3,15 +3,15 @@ package profileutil import ( "testing" - "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/exportoptions" + "github.com/bitrise-io/go-xcode/v2/plistutil" "github.com/stretchr/testify/require" ) func TestPlistData(t *testing.T) { t.Log("development profile specifies development export method") { - profile, err := plistutil.NewPlistDataFromContent(developmentProfileContent) + profile, err := plistutil.NewMapDataFromPlistContent(developmentProfileContent) require.NoError(t, err) require.Equal(t, "4b617a5f-e31e-4edc-9460-718a5abacd05", PlistData(profile).GetUUID()) require.Equal(t, "Bitrise Test Development", PlistData(profile).GetName()) @@ -29,7 +29,7 @@ func TestPlistData(t *testing.T) { t.Log("app store profile specifies app-store export method") { - profile, err := plistutil.NewPlistDataFromContent(appStoreProfileContent) + profile, err := plistutil.NewMapDataFromPlistContent(appStoreProfileContent) require.NoError(t, err) require.Equal(t, "a60668dd-191a-4770-8b1e-b453b87aa60b", PlistData(profile).GetUUID()) require.Equal(t, "Bitrise Test App Store", PlistData(profile).GetName()) @@ -47,7 +47,7 @@ func TestPlistData(t *testing.T) { t.Log("ad hoc profile specifies ad-hoc export method") { - profile, err := plistutil.NewPlistDataFromContent(adHocProfileContent) + profile, err := plistutil.NewMapDataFromPlistContent(adHocProfileContent) require.NoError(t, err) require.Equal(t, "26668300-5743-46a1-8e00-7023e2e35c7d", PlistData(profile).GetUUID()) require.Equal(t, "Bitrise Test Ad Hoc", PlistData(profile).GetName()) @@ -65,7 +65,7 @@ func TestPlistData(t *testing.T) { t.Log("it creates model from enterprise profile content") { - profile, err := plistutil.NewPlistDataFromContent(enterpriseProfileContent) + profile, err := plistutil.NewMapDataFromPlistContent(enterpriseProfileContent) require.NoError(t, err) require.Equal(t, "8d6caa15-ac49-48f9-9bd3-ce9244add6a0", PlistData(profile).GetUUID()) require.Equal(t, "Bitrise Test Enterprise", PlistData(profile).GetName()) @@ -85,7 +85,7 @@ func TestPlistData(t *testing.T) { func TestTVOSPlistData(t *testing.T) { t.Log("it creates model from tvOS appstore profile content") { - profile, err := plistutil.NewPlistDataFromContent(tvOSAppStoreProfileContent) + profile, err := plistutil.NewMapDataFromPlistContent(tvOSAppStoreProfileContent) require.NoError(t, err) require.Equal(t, "dec523d5-624b-44bd-8d16-6d1d69c63276", PlistData(profile).GetUUID()) require.Equal(t, "Bitrise app-store - (bdh.NPO-Live.bitrise.sample)", PlistData(profile).GetName()) diff --git a/xcarchive/ios.go b/xcarchive/ios.go index 61e7f511..0682cf1d 100644 --- a/xcarchive/ios.go +++ b/xcarchive/ios.go @@ -6,16 +6,16 @@ import ( "path/filepath" "github.com/bitrise-io/go-utils/pathutil" - "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" + "github.com/bitrise-io/go-xcode/v2/plistutil" "github.com/bitrise-io/go-xcode/v2/profileutil" ) // IosBaseApplication ... type IosBaseApplication struct { Path string - InfoPlist plistutil.PlistData - Entitlements plistutil.PlistData + InfoPlist plistutil.MapData + Entitlements plistutil.MapData ProvisioningProfile profileutil.ProvisioningProfileInfoModel } @@ -27,7 +27,7 @@ func (app IosBaseApplication) BundleIdentifier() string { // NewIosBaseApplication ... func NewIosBaseApplication(path string) (IosBaseApplication, error) { - var infoPlist plistutil.PlistData + var infoPlist plistutil.MapData { infoPlistPath := filepath.Join(path, "Info.plist") if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { @@ -35,7 +35,7 @@ func NewIosBaseApplication(path string) (IosBaseApplication, error) { } else if !exist { return IosBaseApplication{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) } - plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + plist, err := plistutil.NewMapDataFromPlistFile(infoPlistPath) if err != nil { return IosBaseApplication{}, err } @@ -217,13 +217,13 @@ func NewIosApplication(path string) (IosApplication, error) { // IosArchive ... type IosArchive struct { Path string - InfoPlist plistutil.PlistData + InfoPlist plistutil.MapData Application IosApplication } // NewIosArchive ... func NewIosArchive(path string) (IosArchive, error) { - var infoPlist plistutil.PlistData + var infoPlist plistutil.MapData { infoPlistPath := filepath.Join(path, "Info.plist") if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { @@ -231,7 +231,7 @@ func NewIosArchive(path string) (IosArchive, error) { } else if !exist { return IosArchive{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) } - plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + plist, err := plistutil.NewMapDataFromPlistFile(infoPlistPath) if err != nil { return IosArchive{}, err } @@ -269,7 +269,7 @@ func NewIosArchive(path string) (IosArchive, error) { }, nil } -func applicationFromPlist(InfoPlist plistutil.PlistData) (string, bool) { +func applicationFromPlist(InfoPlist plistutil.MapData) (string, bool) { if properties, found := InfoPlist.GetMapStringInterface("ApplicationProperties"); found { return properties.GetString("ApplicationPath") } @@ -303,8 +303,8 @@ func (archive IosArchive) SigningIdentity() string { } // BundleIDEntitlementsMap ... -func (archive IosArchive) BundleIDEntitlementsMap() map[string]plistutil.PlistData { - bundleIDEntitlementsMap := map[string]plistutil.PlistData{} +func (archive IosArchive) BundleIDEntitlementsMap() map[string]plistutil.MapData { + bundleIDEntitlementsMap := map[string]plistutil.MapData{} bundleID := archive.Application.BundleIdentifier() bundleIDEntitlementsMap[bundleID] = archive.Application.Entitlements diff --git a/xcarchive/ios_test.go b/xcarchive/ios_test.go index 657e4eff..924fab24 100644 --- a/xcarchive/ios_test.go +++ b/xcarchive/ios_test.go @@ -9,8 +9,8 @@ import ( "github.com/bitrise-io/go-utils/command" "github.com/bitrise-io/go-utils/pathutil" - "github.com/bitrise-io/go-xcode/plistutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" + "github.com/bitrise-io/go-xcode/v2/plistutil" "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/stretchr/testify/require" ) @@ -220,14 +220,14 @@ func Test_applicationFromArchive(t *testing.T) { } func Test_applicationFromPlist(t *testing.T) { - infoPlist, err := plistutil.NewPlistDataFromFile(filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive/Info.plist")) + infoPlist, err := plistutil.NewMapDataFromPlistFile(filepath.Join(sampleRepoPath(t), "archives/ios.xcarchive/Info.plist")) const appRelativePathToProduct = "Applications/code-sign-test.app" if err != nil { t.Errorf("setup: could not read plist, error: %s", infoPlist) } type args struct { - InfoPlist plistutil.PlistData + InfoPlist plistutil.MapData } tests := []struct { name string diff --git a/xcarchive/macos.go b/xcarchive/macos.go index 714e94a1..bf153c89 100644 --- a/xcarchive/macos.go +++ b/xcarchive/macos.go @@ -4,7 +4,7 @@ import ( "fmt" "path/filepath" - "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/v2/plistutil" "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/bitrise-io/go-utils/pathutil" @@ -12,8 +12,8 @@ import ( type macosBaseApplication struct { Path string - InfoPlist plistutil.PlistData - Entitlements plistutil.PlistData + InfoPlist plistutil.MapData + Entitlements plistutil.MapData ProvisioningProfile *profileutil.ProvisioningProfileInfoModel } @@ -24,7 +24,7 @@ func (app macosBaseApplication) BundleIdentifier() string { } func newMacosBaseApplication(path string) (macosBaseApplication, error) { - var infoPlist plistutil.PlistData + var infoPlist plistutil.MapData { infoPlistPath := filepath.Join(path, "Contents/Info.plist") if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { @@ -32,7 +32,7 @@ func newMacosBaseApplication(path string) (macosBaseApplication, error) { } else if !exist { return macosBaseApplication{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) } - plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + plist, err := plistutil.NewMapDataFromPlistFile(infoPlistPath) if err != nil { return macosBaseApplication{}, err } @@ -123,13 +123,13 @@ func NewMacosApplication(path string) (MacosApplication, error) { // MacosArchive ... type MacosArchive struct { Path string - InfoPlist plistutil.PlistData + InfoPlist plistutil.MapData Application MacosApplication } // NewMacosArchive ... func NewMacosArchive(path string) (MacosArchive, error) { - var infoPlist plistutil.PlistData + var infoPlist plistutil.MapData { infoPlistPath := filepath.Join(path, "Info.plist") if exist, err := pathutil.IsPathExists(infoPlistPath); err != nil { @@ -137,7 +137,7 @@ func NewMacosArchive(path string) (MacosArchive, error) { } else if !exist { return MacosArchive{}, fmt.Errorf("Info.plist not exists at: %s", infoPlistPath) } - plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + plist, err := plistutil.NewMapDataFromPlistFile(infoPlistPath) if err != nil { return MacosArchive{}, err } @@ -192,8 +192,8 @@ func (archive MacosArchive) SigningIdentity() string { } // BundleIDEntitlementsMap ... -func (archive MacosArchive) BundleIDEntitlementsMap() map[string]plistutil.PlistData { - bundleIDEntitlementsMap := map[string]plistutil.PlistData{} +func (archive MacosArchive) BundleIDEntitlementsMap() map[string]plistutil.MapData { + bundleIDEntitlementsMap := map[string]plistutil.MapData{} bundleID := archive.Application.BundleIdentifier() bundleIDEntitlementsMap[bundleID] = archive.Application.Entitlements diff --git a/xcarchive/utils.go b/xcarchive/utils.go index 987257c7..8933e6b9 100644 --- a/xcarchive/utils.go +++ b/xcarchive/utils.go @@ -7,30 +7,30 @@ import ( "github.com/bitrise-io/go-utils/command" "github.com/bitrise-io/go-utils/pathutil" - "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/v2/plistutil" ) -func executableNameFromInfoPlist(infoPlist plistutil.PlistData) string { +func executableNameFromInfoPlist(infoPlist plistutil.MapData) string { if name, ok := infoPlist.GetString("CFBundleExecutable"); ok { return name } return "" } -func getEntitlements(basePath, executableRelativePath string) (plistutil.PlistData, error) { +func getEntitlements(basePath, executableRelativePath string) (plistutil.MapData, error) { entitlements, err := entitlementsFromExecutable(basePath, executableRelativePath) if err != nil { - return plistutil.PlistData{}, err + return plistutil.MapData{}, err } if entitlements != nil { return *entitlements, nil } - return plistutil.PlistData{}, nil + return plistutil.MapData{}, nil } -func entitlementsFromExecutable(basePath, executableRelativePath string) (*plistutil.PlistData, error) { +func entitlementsFromExecutable(basePath, executableRelativePath string) (*plistutil.MapData, error) { fmt.Printf("Fetching entitlements from executable") cmd := command.New("codesign", "--display", "--entitlements", ":-", filepath.Join(basePath, executableRelativePath)) @@ -39,7 +39,7 @@ func entitlementsFromExecutable(basePath, executableRelativePath string) (*plist return nil, err } - plist, err := plistutil.NewPlistDataFromContent(entitlementsString) + plist, err := plistutil.NewMapDataFromPlistContent(entitlementsString) if err != nil { return nil, err } diff --git a/xcarchive/utils_test.go b/xcarchive/utils_test.go index 19cf88aa..e8c13020 100644 --- a/xcarchive/utils_test.go +++ b/xcarchive/utils_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/bitrise-io/go-utils/pathutil" - "github.com/bitrise-io/go-xcode/plistutil" + "github.com/bitrise-io/go-xcode/v2/plistutil" "github.com/stretchr/testify/assert" ) @@ -53,7 +53,7 @@ func executableRelativePath(basePath, infoPlistRelativePath, executableFolderRel return "" } - plist, err := plistutil.NewPlistDataFromFile(infoPlistPath) + plist, err := plistutil.NewMapDataFromPlistFile(infoPlistPath) if err != nil { return "" } @@ -61,7 +61,7 @@ func executableRelativePath(basePath, infoPlistRelativePath, executableFolderRel return filepath.Join(executableFolderRelativePath, executableNameFromInfoPlist(plist)) } -func iosEntitlements() plistutil.PlistData { +func iosEntitlements() plistutil.MapData { return map[string]interface{}{ "application-identifier": "72SA8V3WYL.io.bitrise.appcliptest", "com.apple.developer.applesignin": []interface{}{"Default"}, @@ -72,7 +72,7 @@ func iosEntitlements() plistutil.PlistData { } } -func macosEntitlements() plistutil.PlistData { +func macosEntitlements() plistutil.MapData { return map[string]interface{}{ "com.apple.security.app-sandbox": true, "com.apple.security.files.user-selected.read-only": true, From e93b98b88f5dac69cceeea36ba0efd1f37d998c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krisztia=CC=81n=20Go=CC=88drei?= Date: Fri, 20 Jun 2025 15:45:38 +0200 Subject: [PATCH 6/7] Rename CertificateInfoModel to CertificateInfo and removed unused functions --- autocodesign/autocodesign.go | 10 ++-- autocodesign/autocodesign_test.go | 6 +-- autocodesign/certdownloader/certdownloader.go | 8 +-- .../certdownloader/certdownloader_test.go | 6 +-- autocodesign/certificates.go | 12 ++--- autocodesign/certificates_test.go | 6 +-- autocodesign/codesignasset/writer.go | 2 +- autocodesign/keychain/keychain.go | 2 +- .../localcodesignasset_test.go | 18 +++---- .../localcodesignasset/profilelookup_test.go | 10 ++-- autocodesign/mock_AssetWriter.go | 4 +- autocodesign/mock_CertificateProvider.go | 8 +-- autocodesign/utils_test.go | 2 +- certificateutil/info_model.go | 35 ++++--------- certificateutil/security_tool.go | 16 +++--- certificateutil/util.go | 14 ++--- certificateutil/util_test.go | 52 +++++++++---------- codesign/codesign.go | 8 +-- codesign/codesign_test.go | 14 ++--- export/codesing_group.go | 4 +- export/export.go | 8 +-- export/ios.go | 8 +-- export/mac.go | 14 ++--- exportoptionsgenerator/certificates.go | 4 +- .../exportoptionsgenerator_test.go | 16 +++--- profileutil/info_model.go | 6 +-- 26 files changed, 139 insertions(+), 154 deletions(-) diff --git a/autocodesign/autocodesign.go b/autocodesign/autocodesign.go index 0da553cc..43fe6b38 100644 --- a/autocodesign/autocodesign.go +++ b/autocodesign/autocodesign.go @@ -31,7 +31,7 @@ type Profile interface { type AppCodesignAssets struct { ArchivableTargetProfilesByBundleID map[string]Profile UITestTargetProfilesByBundleID map[string]Profile - Certificate certificateutil.CertificateInfoModel + Certificate certificateutil.CertificateInfo } // Platform ... @@ -63,7 +63,7 @@ type Entitlements serialized.Object // Certificate is certificate present on Apple App Store Connect API, could match a local certificate type Certificate struct { - CertificateInfo certificateutil.CertificateInfoModel + CertificateInfo certificateutil.CertificateInfo ID string } @@ -96,7 +96,7 @@ type DevPortalClient interface { // AssetWriter ... type AssetWriter interface { Write(codesignAssetsByDistributionType map[DistributionType]AppCodesignAssets) error - InstallCertificate(certificate certificateutil.CertificateInfoModel) error + InstallCertificate(certificate certificateutil.CertificateInfo) error InstallProfile(profile Profile) error } @@ -114,11 +114,11 @@ type AppLayout struct { // CertificateProvider returns codesigning certificates (with private key) type CertificateProvider interface { - GetCertificates() ([]certificateutil.CertificateInfoModel, error) + GetCertificates() ([]certificateutil.CertificateInfo, error) } // LocalCertificates is a map from the certificate type (development, distribution) to an array of installed certs -type LocalCertificates map[appstoreconnect.CertificateType][]certificateutil.CertificateInfoModel +type LocalCertificates map[appstoreconnect.CertificateType][]certificateutil.CertificateInfo // LocalProfile ... type LocalProfile struct { diff --git a/autocodesign/autocodesign_test.go b/autocodesign/autocodesign_test.go index eedbc4e5..ebf46232 100644 --- a/autocodesign/autocodesign_test.go +++ b/autocodesign/autocodesign_test.go @@ -62,7 +62,7 @@ func newMockProfile(m profileArgs) Profile { return profile } -func newCertificate(t *testing.T, teamID, teamName, commonName string, expiry time.Time) certificateutil.CertificateInfoModel { +func newCertificate(t *testing.T, teamID, teamName, commonName string, expiry time.Time) certificateutil.CertificateInfo { cert, privateKey, err := certificateutil.GenerateTestCertificate(int64(1), teamID, teamName, commonName, expiry) if err != nil { t.Fatalf("init: failed to generate certificate: %s", err) @@ -511,7 +511,7 @@ func Test_GivenLocalProfile_WhenCertificateIsMissing_ThenInstalled(t *testing.T) require.Equal(t, wantAssets, gotAssets) } -func newClientWithoutAppIDAndProfile(cert certificateutil.CertificateInfoModel) *MockDevPortalClient { +func newClientWithoutAppIDAndProfile(cert certificateutil.CertificateInfo) *MockDevPortalClient { client := newMockDevportalClient(devportalArgs{ certs: map[appstoreconnect.CertificateType][]Certificate{ appstoreconnect.IOSDevelopment: {{ @@ -528,7 +528,7 @@ func newClientWithoutAppIDAndProfile(cert certificateutil.CertificateInfoModel) return client } -func newClientWithAppIDWithoutAppleSignIn(cert certificateutil.CertificateInfoModel, bundleID string) *MockDevPortalClient { +func newClientWithAppIDWithoutAppleSignIn(cert certificateutil.CertificateInfo, bundleID string) *MockDevPortalClient { appID := appstoreconnect.BundleID{ Attributes: appstoreconnect.BundleIDAttributes{ Identifier: bundleID, diff --git a/autocodesign/certdownloader/certdownloader.go b/autocodesign/certdownloader/certdownloader.go index d5e893da..507a39f3 100644 --- a/autocodesign/certdownloader/certdownloader.go +++ b/autocodesign/certdownloader/certdownloader.go @@ -33,8 +33,8 @@ func NewDownloader(certs []CertificateAndPassphrase, client *http.Client) autoco } // GetCertificates ... -func (d downloader) GetCertificates() ([]certificateutil.CertificateInfoModel, error) { - var certInfos []certificateutil.CertificateInfoModel +func (d downloader) GetCertificates() ([]certificateutil.CertificateInfo, error) { + var certInfos []certificateutil.CertificateInfo for i, p12 := range d.certs { log.Debugf("Downloading p12 file number %d from %s", i, p12.URL) @@ -52,7 +52,7 @@ func (d downloader) GetCertificates() ([]certificateutil.CertificateInfoModel, e } // downloadAndParsePKCS12 downloads a pkcs12 format file and parses certificates and matching private keys. -func downloadAndParsePKCS12(httpClient *http.Client, certificateURL, passphrase string) ([]certificateutil.CertificateInfoModel, error) { +func downloadAndParsePKCS12(httpClient *http.Client, certificateURL, passphrase string) ([]certificateutil.CertificateInfo, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() @@ -74,7 +74,7 @@ func downloadAndParsePKCS12(httpClient *http.Client, certificateURL, passphrase return infos, nil } -func certsToString(certs []certificateutil.CertificateInfoModel) (s string) { +func certsToString(certs []certificateutil.CertificateInfo) (s string) { for i, cert := range certs { s += "- " s += cert.String() diff --git a/autocodesign/certdownloader/certdownloader_test.go b/autocodesign/certdownloader/certdownloader_test.go index c99f4055..371b0e0f 100644 --- a/autocodesign/certdownloader/certdownloader_test.go +++ b/autocodesign/certdownloader/certdownloader_test.go @@ -44,7 +44,7 @@ func Test_downloader_GetCertificates_Local(t *testing.T) { } got, err := d.GetCertificates() - want := []certificateutil.CertificateInfoModel{ + want := []certificateutil.CertificateInfo{ certInfo, } @@ -83,7 +83,7 @@ func Test_downloader_GetCertificates_Remote(t *testing.T) { } got, err := d.GetCertificates() - want := []certificateutil.CertificateInfoModel{ + want := []certificateutil.CertificateInfo{ certInfo, } @@ -91,7 +91,7 @@ func Test_downloader_GetCertificates_Remote(t *testing.T) { assert.Equal(t, want, got) } -func createTestCert(t *testing.T) certificateutil.CertificateInfoModel { +func createTestCert(t *testing.T) certificateutil.CertificateInfo { const ( teamID = "MYTEAMID" commonName = "Apple Developer: test" diff --git a/autocodesign/certificates.go b/autocodesign/certificates.go index 031a0f3f..0fdb6f13 100644 --- a/autocodesign/certificates.go +++ b/autocodesign/certificates.go @@ -90,7 +90,7 @@ func getValidCertificates(typeToLocalCerts LocalCertificates, client DevPortalCl } // GetValidLocalCertificates returns validated and deduplicated local certificates -func GetValidLocalCertificates(certificates []certificateutil.CertificateInfoModel) (LocalCertificates, error) { +func GetValidLocalCertificates(certificates []certificateutil.CertificateInfo) (LocalCertificates, error) { preFilteredCerts := certificateutil.FilterValidCertificateInfos(certificates) if len(preFilteredCerts.InvalidCertificates) != 0 { @@ -113,7 +113,7 @@ func GetValidLocalCertificates(certificates []certificateutil.CertificateInfoMod } // matchLocalToAPICertificates ... -func matchLocalToAPICertificates(client DevPortalClient, localCertificates []certificateutil.CertificateInfoModel) []Certificate { +func matchLocalToAPICertificates(client DevPortalClient, localCertificates []certificateutil.CertificateInfo) []Certificate { var matchingCertificates []Certificate for _, localCert := range localCertificates { @@ -150,9 +150,9 @@ func logAllAPICertificates(client DevPortalClient) error { } // filterCertificates returns the certificates matching to the given common name, developer team ID, and distribution type. -func filterCertificates(certificates []certificateutil.CertificateInfoModel, certificateType appstoreconnect.CertificateType) []certificateutil.CertificateInfoModel { +func filterCertificates(certificates []certificateutil.CertificateInfo, certificateType appstoreconnect.CertificateType) []certificateutil.CertificateInfo { // filter by distribution type - var filteredCertificates []certificateutil.CertificateInfoModel + var filteredCertificates []certificateutil.CertificateInfo for _, certificate := range certificates { if certificateType == appstoreconnect.IOSDistribution && isDistributionCertificate(certificate) { filteredCertificates = append(filteredCertificates, certificate) @@ -178,13 +178,13 @@ func filterCertificates(certificates []certificateutil.CertificateInfoModel, cer return filteredCertificates } -func isDistributionCertificate(cert certificateutil.CertificateInfoModel) bool { +func isDistributionCertificate(cert certificateutil.CertificateInfo) bool { // Apple certificate types: https://help.apple.com/xcode/mac/current/#/dev80c6204ec) return strings.HasPrefix(strings.ToLower(cert.CommonName), strings.ToLower("iPhone Distribution")) || strings.HasPrefix(strings.ToLower(cert.CommonName), strings.ToLower("Apple Distribution")) } -func certsToString(certs []certificateutil.CertificateInfoModel) (s string) { +func certsToString(certs []certificateutil.CertificateInfo) (s string) { for i, cert := range certs { s += "- " s += cert.String() diff --git a/autocodesign/certificates_test.go b/autocodesign/certificates_test.go index 1ca137f9..dfe32c20 100644 --- a/autocodesign/certificates_test.go +++ b/autocodesign/certificates_test.go @@ -364,13 +364,13 @@ func TestGetValidLocalCertificates(t *testing.T) { tests := []struct { name string - certificates []certificateutil.CertificateInfoModel + certificates []certificateutil.CertificateInfo want LocalCertificates wantErr bool }{ { name: "Duplicate certificate (same name)", - certificates: []certificateutil.CertificateInfoModel{ + certificates: []certificateutil.CertificateInfo{ devCert, devCert, devCert2, @@ -385,7 +385,7 @@ func TestGetValidLocalCertificates(t *testing.T) { }, { name: "dev + dist cert", - certificates: []certificateutil.CertificateInfoModel{ + certificates: []certificateutil.CertificateInfo{ distributionCert, devCert, }, diff --git a/autocodesign/codesignasset/writer.go b/autocodesign/codesignasset/writer.go index eee8d929..5a746282 100644 --- a/autocodesign/codesignasset/writer.go +++ b/autocodesign/codesignasset/writer.go @@ -70,7 +70,7 @@ func (w Writer) Write(codesignAssetsByDistributionType map[autocodesign.Distribu } // InstallCertificate installs the certificate to the Keychain -func (w Writer) InstallCertificate(certificate certificateutil.CertificateInfoModel) error { +func (w Writer) InstallCertificate(certificate certificateutil.CertificateInfo) error { // Empty passphrase provided, as already parsed certificate + private key return w.keychain.InstallCertificate(certificate, "") } diff --git a/autocodesign/keychain/keychain.go b/autocodesign/keychain/keychain.go index e8dad26e..09188625 100644 --- a/autocodesign/keychain/keychain.go +++ b/autocodesign/keychain/keychain.go @@ -50,7 +50,7 @@ func New(pth string, pass stepconf.Secret, factory command.Factory) (*Keychain, } // InstallCertificate ... -func (k Keychain) InstallCertificate(cert certificateutil.CertificateInfoModel, pass stepconf.Secret) error { +func (k Keychain) InstallCertificate(cert certificateutil.CertificateInfo, pass stepconf.Secret) error { b, err := cert.EncodeToP12("bitrise") if err != nil { return err diff --git a/autocodesign/localcodesignasset/localcodesignasset_test.go b/autocodesign/localcodesignasset/localcodesignasset_test.go index 406a23c1..ba1668d2 100644 --- a/autocodesign/localcodesignasset/localcodesignasset_test.go +++ b/autocodesign/localcodesignasset/localcodesignasset_test.go @@ -177,7 +177,7 @@ func createTestObjects(t *testing.T) (Manager, []profileutil.ProvisioningProfile return NewManager(mockProvider, mockConverter), profiles } -func findCert(t *testing.T, certsByType map[appstoreconnect.CertificateType][]autocodesign.Certificate, serial string) certificateutil.CertificateInfoModel { +func findCert(t *testing.T, certsByType map[appstoreconnect.CertificateType][]autocodesign.Certificate, serial string) certificateutil.CertificateInfo { for _, certs := range certsByType { for _, cert := range certs { if cert.CertificateInfo.Serial == serial { @@ -188,7 +188,7 @@ func findCert(t *testing.T, certsByType map[appstoreconnect.CertificateType][]au t.Fatalf("missing certificate") - return certificateutil.CertificateInfoModel{} + return certificateutil.CertificateInfo{} } func findProvProfile(t *testing.T, profiles []profileutil.ProvisioningProfileInfoModel, uuid string) autocodesign.Profile { @@ -228,7 +228,7 @@ func profiles(t *testing.T) []profileutil.ProvisioningProfileInfoModel { BundleID: "io.ios.valid", ExportType: exportoptions.MethodDevelopment, ProvisionedDevices: []string{"device-1", "device-2", "device-3"}, - DeveloperCertificates: []certificateutil.CertificateInfoModel{devCert(t, dateRelativeToNow(1, 0, 0))}, + DeveloperCertificates: []certificateutil.CertificateInfo{devCert(t, dateRelativeToNow(1, 0, 0))}, CreationDate: dateRelativeToNow(0, -1, 0), ExpirationDate: dateRelativeToNow(0, 1, 0), Entitlements: entitlements(), @@ -243,7 +243,7 @@ func profiles(t *testing.T) []profileutil.ProvisioningProfileInfoModel { BundleID: "io.tvos.valid", ExportType: exportoptions.MethodAppStore, ProvisionedDevices: nil, - DeveloperCertificates: []certificateutil.CertificateInfoModel{distCert(t, dateRelativeToNow(1, 0, 0))}, + DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, dateRelativeToNow(1, 0, 0))}, CreationDate: dateRelativeToNow(0, -1, 0), ExpirationDate: dateRelativeToNow(0, 1, 0), Entitlements: nil, @@ -258,7 +258,7 @@ func profiles(t *testing.T) []profileutil.ProvisioningProfileInfoModel { BundleID: "io.tvos.expired", ExportType: exportoptions.MethodAppStore, ProvisionedDevices: nil, - DeveloperCertificates: []certificateutil.CertificateInfoModel{distCert(t, dateRelativeToNow(1, 0, 0))}, + DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, dateRelativeToNow(1, 0, 0))}, CreationDate: dateRelativeToNow(0, -1, 0), ExpirationDate: dateRelativeToNow(0, 0, -1), Entitlements: nil, @@ -273,7 +273,7 @@ func profiles(t *testing.T) []profileutil.ProvisioningProfileInfoModel { BundleID: "io.ios.*", ExportType: exportoptions.MethodDevelopment, ProvisionedDevices: []string{"device-1", "device-2", "device-3"}, - DeveloperCertificates: []certificateutil.CertificateInfoModel{devCert(t, dateRelativeToNow(1, 0, 0))}, + DeveloperCertificates: []certificateutil.CertificateInfo{devCert(t, dateRelativeToNow(1, 0, 0))}, CreationDate: dateRelativeToNow(0, -1, 0), ExpirationDate: dateRelativeToNow(0, 1, 0), Entitlements: entitlements(), @@ -310,15 +310,15 @@ func certsByType(t *testing.T) map[appstoreconnect.CertificateType][]autocodesig } } -func devCert(t *testing.T, expiry time.Time) certificateutil.CertificateInfoModel { +func devCert(t *testing.T, expiry time.Time) certificateutil.CertificateInfo { return newCertificate(t, 1, teamID, teamName, "Development certificate", expiry) } -func distCert(t *testing.T, expiry time.Time) certificateutil.CertificateInfoModel { +func distCert(t *testing.T, expiry time.Time) certificateutil.CertificateInfo { return newCertificate(t, 2, teamID, teamName, "Distribution certificate", expiry) } -func newCertificate(t *testing.T, serial int, teamID, teamName, commonName string, expiry time.Time) certificateutil.CertificateInfoModel { +func newCertificate(t *testing.T, serial int, teamID, teamName, commonName string, expiry time.Time) certificateutil.CertificateInfo { cert, privateKey, err := certificateutil.GenerateTestCertificate(int64(serial), teamID, teamName, commonName, expiry) if err != nil { t.Fatalf("init: failed to generate certificate: %s", err) diff --git a/autocodesign/localcodesignasset/profilelookup_test.go b/autocodesign/localcodesignasset/profilelookup_test.go index 9aad5716..10eaaa2b 100644 --- a/autocodesign/localcodesignasset/profilelookup_test.go +++ b/autocodesign/localcodesignasset/profilelookup_test.go @@ -206,7 +206,7 @@ func iosXcodeManagedDevelopmentProfile(t *testing.T) profileutil.ProvisioningPro BundleID: "io.ios.managed", ExportType: exportoptions.MethodDevelopment, ProvisionedDevices: []string{"ios-device-1", "ios-device-2", "ios-device-3"}, - DeveloperCertificates: []certificateutil.CertificateInfoModel{devCert(t, dateRelativeToNow(1, 0, 0))}, + DeveloperCertificates: []certificateutil.CertificateInfo{devCert(t, dateRelativeToNow(1, 0, 0))}, CreationDate: dateRelativeToNow(0, 0, -1), ExpirationDate: dateRelativeToNow(0, 0, 5), Entitlements: firstSetOfEntitlements(), @@ -224,7 +224,7 @@ func iosDevelopmentProfile(t *testing.T) profileutil.ProvisioningProfileInfoMode BundleID: "io.ios", ExportType: exportoptions.MethodDevelopment, ProvisionedDevices: []string{"ios-device-1", "ios-device-2", "ios-device-3"}, - DeveloperCertificates: []certificateutil.CertificateInfoModel{devCert(t, dateRelativeToNow(1, 0, 0))}, + DeveloperCertificates: []certificateutil.CertificateInfo{devCert(t, dateRelativeToNow(1, 0, 0))}, CreationDate: dateRelativeToNow(0, 0, -1), ExpirationDate: dateRelativeToNow(0, 0, 5), Entitlements: firstSetOfEntitlements(), @@ -242,7 +242,7 @@ func iosAppStoreProfile(t *testing.T) profileutil.ProvisioningProfileInfoModel { BundleID: "io.ios", ExportType: exportoptions.MethodAppStore, ProvisionedDevices: nil, - DeveloperCertificates: []certificateutil.CertificateInfoModel{distCert(t, dateRelativeToNow(1, 0, 0))}, + DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, dateRelativeToNow(1, 0, 0))}, CreationDate: dateRelativeToNow(0, 0, -1), ExpirationDate: dateRelativeToNow(0, 0, 5), Entitlements: nil, @@ -260,7 +260,7 @@ func tvosAdHocProfile(t *testing.T) profileutil.ProvisioningProfileInfoModel { BundleID: "io.tvos", ExportType: exportoptions.MethodAdHoc, ProvisionedDevices: []string{"tvos-device-1", "tvos-device-2", "tvos-device-3"}, - DeveloperCertificates: []certificateutil.CertificateInfoModel{distCert(t, dateRelativeToNow(1, 0, 0))}, + DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, dateRelativeToNow(1, 0, 0))}, CreationDate: dateRelativeToNow(0, 0, -1), ExpirationDate: dateRelativeToNow(0, 0, 10), Entitlements: nil, @@ -278,7 +278,7 @@ func tvosEnterpriseProfile(t *testing.T) profileutil.ProvisioningProfileInfoMode BundleID: "io.tvos", ExportType: exportoptions.MethodEnterprise, ProvisionedDevices: nil, - DeveloperCertificates: []certificateutil.CertificateInfoModel{distCert(t, dateRelativeToNow(1, 0, 0))}, + DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, dateRelativeToNow(1, 0, 0))}, CreationDate: dateRelativeToNow(0, 0, -1), ExpirationDate: dateRelativeToNow(0, 0, 10), Entitlements: secondSetOfEntitlements(), diff --git a/autocodesign/mock_AssetWriter.go b/autocodesign/mock_AssetWriter.go index c70f19fd..f77e5da3 100644 --- a/autocodesign/mock_AssetWriter.go +++ b/autocodesign/mock_AssetWriter.go @@ -13,11 +13,11 @@ type MockAssetWriter struct { } // InstallCertificate provides a mock function with given fields: certificate -func (_m *MockAssetWriter) InstallCertificate(certificate certificateutil.CertificateInfoModel) error { +func (_m *MockAssetWriter) InstallCertificate(certificate certificateutil.CertificateInfo) error { ret := _m.Called(certificate) var r0 error - if rf, ok := ret.Get(0).(func(certificateutil.CertificateInfoModel) error); ok { + if rf, ok := ret.Get(0).(func(certificateutil.CertificateInfo) error); ok { r0 = rf(certificate) } else { r0 = ret.Error(0) diff --git a/autocodesign/mock_CertificateProvider.go b/autocodesign/mock_CertificateProvider.go index b6d9347a..ef747bec 100644 --- a/autocodesign/mock_CertificateProvider.go +++ b/autocodesign/mock_CertificateProvider.go @@ -13,15 +13,15 @@ type MockCertificateProvider struct { } // GetCertificates provides a mock function with given fields: -func (_m *MockCertificateProvider) GetCertificates() ([]certificateutil.CertificateInfoModel, error) { +func (_m *MockCertificateProvider) GetCertificates() ([]certificateutil.CertificateInfo, error) { ret := _m.Called() - var r0 []certificateutil.CertificateInfoModel - if rf, ok := ret.Get(0).(func() []certificateutil.CertificateInfoModel); ok { + var r0 []certificateutil.CertificateInfo + if rf, ok := ret.Get(0).(func() []certificateutil.CertificateInfo); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0, ok = ret.Get(0).([]certificateutil.CertificateInfoModel) + r0, ok = ret.Get(0).([]certificateutil.CertificateInfo) if !ok { } } diff --git a/autocodesign/utils_test.go b/autocodesign/utils_test.go index 59126157..463976da 100644 --- a/autocodesign/utils_test.go +++ b/autocodesign/utils_test.go @@ -17,7 +17,7 @@ func Test_GivenCodeSignAssets_WhenMergingTwo_ThenValuesAreCorrect(t *testing.T) enterprise1Profile := profile("enterprise", "1") adHoc1Profile := profile("ad-hoc", "1") - certificate := certificateutil.CertificateInfoModel{} + certificate := certificateutil.CertificateInfo{} tests := []struct { name string base *AppCodesignAssets diff --git a/certificateutil/info_model.go b/certificateutil/info_model.go index fe54ede6..fba4df1e 100644 --- a/certificateutil/info_model.go +++ b/certificateutil/info_model.go @@ -10,11 +10,9 @@ import ( "time" "github.com/bitrise-io/go-pkcs12" - "github.com/bitrise-io/go-utils/fileutil" ) -// CertificateInfoModel ... -type CertificateInfoModel struct { +type CertificateInfo struct { CommonName string TeamName string TeamID string @@ -28,12 +26,11 @@ type CertificateInfoModel struct { PrivateKey interface{} } -// NewCertificateInfo ... -func NewCertificateInfo(certificate x509.Certificate, privateKey interface{}) CertificateInfoModel { +func NewCertificateInfo(certificate x509.Certificate, privateKey interface{}) CertificateInfo { fingerprint := sha1.Sum(certificate.Raw) fingerprintStr := fmt.Sprintf("%x", fingerprint) - return CertificateInfoModel{ + return CertificateInfo{ CommonName: certificate.Subject.CommonName, TeamName: strings.Join(certificate.Subject.Organization, " "), TeamID: strings.Join(certificate.Subject.OrganizationalUnit, " "), @@ -47,9 +44,9 @@ func NewCertificateInfo(certificate x509.Certificate, privateKey interface{}) Ce } } -// CertificatesFromPKCS12Content returns an array of CertificateInfoModel +// CertificatesFromPKCS12Content returns an array of CertificateInfo // Used to parse p12 file containing multiple codesign identities (exported from macOS Keychain) -func CertificatesFromPKCS12Content(content []byte, password string) ([]CertificateInfoModel, error) { +func CertificatesFromPKCS12Content(content []byte, password string) ([]CertificateInfo, error) { privateKeys, certificates, err := pkcs12.DecodeAll(content, password) if err != nil { return nil, err @@ -63,7 +60,7 @@ func CertificatesFromPKCS12Content(content []byte, password string) ([]Certifica return nil, errors.New("pkcs12: no certificate and private key pair found") } - infos := []CertificateInfoModel{} + infos := []CertificateInfo{} for i, certificate := range certificates { if certificate != nil { infos = append(infos, NewCertificateInfo(*certificate, privateKeys[i])) @@ -73,18 +70,7 @@ func CertificatesFromPKCS12Content(content []byte, password string) ([]Certifica return infos, nil } -// CertificatesFromPKCS12File ... -func CertificatesFromPKCS12File(pkcs12Pth, password string) ([]CertificateInfoModel, error) { - content, err := fileutil.ReadBytesFromFile(pkcs12Pth) - if err != nil { - return nil, err - } - - return CertificatesFromPKCS12Content(content, password) -} - -// String ... -func (info CertificateInfoModel) String() string { +func (info CertificateInfo) String() string { team := fmt.Sprintf("%s (%s)", info.TeamName, info.TeamID) certInfo := fmt.Sprintf("Serial: %s, Name: %s, Team: %s, Expiry: %s", info.Serial, info.CommonName, team, info.EndDate) @@ -96,12 +82,11 @@ func (info CertificateInfoModel) String() string { return certInfo } -// CheckValidity ... -func (info CertificateInfoModel) CheckValidity() error { +func (info CertificateInfo) CheckValidity() error { return CheckValidity(info.Certificate) } -// EncodeToP12 encodes a CertificateInfoModel in pkcs12 (.p12) format. -func (info CertificateInfoModel) EncodeToP12(passphrase string) ([]byte, error) { +// EncodeToP12 encodes a CertificateInfo in pkcs12 (.p12) format. +func (info CertificateInfo) EncodeToP12(passphrase string) ([]byte, error) { return pkcs12.Encode(rand.Reader, info.PrivateKey, &info.Certificate, nil, passphrase) } diff --git a/certificateutil/security_tool.go b/certificateutil/security_tool.go index 49fe4c9d..35c06de2 100644 --- a/certificateutil/security_tool.go +++ b/certificateutil/security_tool.go @@ -11,13 +11,13 @@ import ( ) // InstalledCodesigningCertificateInfos ... -func InstalledCodesigningCertificateInfos() ([]CertificateInfoModel, error) { - certificates, err := InstalledCodesigningCertificates() +func InstalledCodesigningCertificateInfos() ([]CertificateInfo, error) { + certificates, err := installedCodesigningCertificates() if err != nil { return nil, err } - infos := []CertificateInfoModel{} + infos := []CertificateInfo{} for _, certificate := range certificates { if certificate != nil { infos = append(infos, NewCertificateInfo(*certificate, nil)) @@ -28,28 +28,28 @@ func InstalledCodesigningCertificateInfos() ([]CertificateInfoModel, error) { } // InstalledInstallerCertificateInfos ... -func InstalledInstallerCertificateInfos() ([]CertificateInfoModel, error) { +func InstalledInstallerCertificateInfos() ([]CertificateInfo, error) { certificates, err := InstalledMacAppStoreCertificates() if err != nil { return nil, err } - infos := []CertificateInfoModel{} + infos := []CertificateInfo{} for _, certificate := range certificates { if certificate != nil { infos = append(infos, NewCertificateInfo(*certificate, nil)) } } - installerCertificates := FilterCertificateInfoModelsByFilterFunc(infos, func(cert CertificateInfoModel) bool { + installerCertificates := FilterCertificateInfoModelsByFilterFunc(infos, func(cert CertificateInfo) bool { return strings.Contains(cert.CommonName, "Installer") }) return installerCertificates, nil } -// InstalledCodesigningCertificates ... -func InstalledCodesigningCertificates() ([]*x509.Certificate, error) { +// installedCodesigningCertificates ... +func installedCodesigningCertificates() ([]*x509.Certificate, error) { certificateNames, err := InstalledCodesigningCertificateNames() if err != nil { return nil, err diff --git a/certificateutil/util.go b/certificateutil/util.go index 9942856f..cb5cd819 100644 --- a/certificateutil/util.go +++ b/certificateutil/util.go @@ -39,8 +39,8 @@ func CheckValidity(certificate x509.Certificate) error { } // FilterCertificateInfoModelsByFilterFunc ... -func FilterCertificateInfoModelsByFilterFunc(certificates []CertificateInfoModel, filterFunc func(certificate CertificateInfoModel) bool) []CertificateInfoModel { - filteredCertificates := []CertificateInfoModel{} +func FilterCertificateInfoModelsByFilterFunc(certificates []CertificateInfo, filterFunc func(certificate CertificateInfo) bool) []CertificateInfo { + filteredCertificates := []CertificateInfo{} for _, certificate := range certificates { if filterFunc(certificate) { @@ -55,13 +55,13 @@ func FilterCertificateInfoModelsByFilterFunc(certificates []CertificateInfoModel type ValidCertificateInfo struct { ValidCertificates, InvalidCertificates, - DuplicatedCertificates []CertificateInfoModel + DuplicatedCertificates []CertificateInfo } // FilterValidCertificateInfos filters out invalid and duplicated common name certificaates -func FilterValidCertificateInfos(certificateInfos []CertificateInfoModel) ValidCertificateInfo { - var invalidCertificates []CertificateInfoModel - nameToCerts := map[string][]CertificateInfoModel{} +func FilterValidCertificateInfos(certificateInfos []CertificateInfo) ValidCertificateInfo { + var invalidCertificates []CertificateInfo + nameToCerts := map[string][]CertificateInfo{} for _, certificateInfo := range certificateInfos { if certificateInfo.CheckValidity() != nil { invalidCertificates = append(invalidCertificates, certificateInfo) @@ -71,7 +71,7 @@ func FilterValidCertificateInfos(certificateInfos []CertificateInfoModel) ValidC nameToCerts[certificateInfo.CommonName] = append(nameToCerts[certificateInfo.CommonName], certificateInfo) } - var validCertificates, duplicatedCertificates []CertificateInfoModel + var validCertificates, duplicatedCertificates []CertificateInfo for _, certs := range nameToCerts { if len(certs) == 0 { continue diff --git a/certificateutil/util_test.go b/certificateutil/util_test.go index ad5e6ece..25e8f99e 100644 --- a/certificateutil/util_test.go +++ b/certificateutil/util_test.go @@ -10,33 +10,33 @@ import ( ) func TestFilterCertificateInfoModelsByFilterFunc(t *testing.T) { - filterableCerts := []CertificateInfoModel{ - CertificateInfoModel{TeamID: "my-team-id"}, - CertificateInfoModel{TeamID: "find-this-team-id"}, - CertificateInfoModel{TeamID: "my--another-team-id"}, - CertificateInfoModel{TeamID: "test-team-id", CommonName: "test common name"}, - CertificateInfoModel{TeamID: "test-team-id2", CommonName: "find this common name"}, + filterableCerts := []CertificateInfo{ + CertificateInfo{TeamID: "my-team-id"}, + CertificateInfo{TeamID: "find-this-team-id"}, + CertificateInfo{TeamID: "my--another-team-id"}, + CertificateInfo{TeamID: "test-team-id", CommonName: "test common name"}, + CertificateInfo{TeamID: "test-team-id2", CommonName: "find this common name"}, } - expectedCertsByTeamID := []CertificateInfoModel{ - CertificateInfoModel{TeamID: "find-this-team-id"}, + expectedCertsByTeamID := []CertificateInfo{ + CertificateInfo{TeamID: "find-this-team-id"}, } - foundCerts := FilterCertificateInfoModelsByFilterFunc(filterableCerts, func(cert CertificateInfoModel) bool { return cert.TeamID == "find-this-team-id" }) + foundCerts := FilterCertificateInfoModelsByFilterFunc(filterableCerts, func(cert CertificateInfo) bool { return cert.TeamID == "find-this-team-id" }) require.Equal(t, expectedCertsByTeamID, foundCerts) - expectedCertsByCommonNameExact := []CertificateInfoModel{ - CertificateInfoModel{TeamID: "test-team-id2", CommonName: "find this common name"}, + expectedCertsByCommonNameExact := []CertificateInfo{ + CertificateInfo{TeamID: "test-team-id2", CommonName: "find this common name"}, } - foundCerts = FilterCertificateInfoModelsByFilterFunc(filterableCerts, func(cert CertificateInfoModel) bool { return cert.CommonName == "find this common name" }) + foundCerts = FilterCertificateInfoModelsByFilterFunc(filterableCerts, func(cert CertificateInfo) bool { return cert.CommonName == "find this common name" }) require.Equal(t, expectedCertsByCommonNameExact, foundCerts) - expectedCertsByCommonNameMatch := []CertificateInfoModel{ - CertificateInfoModel{TeamID: "test-team-id", CommonName: "test common name"}, - CertificateInfoModel{TeamID: "test-team-id2", CommonName: "find this common name"}, + expectedCertsByCommonNameMatch := []CertificateInfo{ + CertificateInfo{TeamID: "test-team-id", CommonName: "test common name"}, + CertificateInfo{TeamID: "test-team-id2", CommonName: "find this common name"}, } - foundCerts = FilterCertificateInfoModelsByFilterFunc(filterableCerts, func(cert CertificateInfoModel) bool { return strings.Contains(cert.CommonName, "common name") }) + foundCerts = FilterCertificateInfoModelsByFilterFunc(filterableCerts, func(cert CertificateInfo) bool { return strings.Contains(cert.CommonName, "common name") }) require.Equal(t, expectedCertsByCommonNameMatch, foundCerts) } @@ -72,34 +72,34 @@ func TestFilterValidCertificateInfos(t *testing.T) { tests := []struct { name string - certificateInfos []CertificateInfoModel + certificateInfos []CertificateInfo want ValidCertificateInfo }{ { name: "one valid cert", - certificateInfos: []CertificateInfoModel{latestValidCertInfo}, + certificateInfos: []CertificateInfo{latestValidCertInfo}, want: ValidCertificateInfo{ - ValidCertificates: []CertificateInfoModel{latestValidCertInfo}, + ValidCertificates: []CertificateInfo{latestValidCertInfo}, InvalidCertificates: nil, DuplicatedCertificates: nil, }, }, { name: "one valid, one invalid cert with same name", - certificateInfos: []CertificateInfoModel{latestValidCertInfo, invalidCertInfo}, + certificateInfos: []CertificateInfo{latestValidCertInfo, invalidCertInfo}, want: ValidCertificateInfo{ - ValidCertificates: []CertificateInfoModel{latestValidCertInfo}, - InvalidCertificates: []CertificateInfoModel{invalidCertInfo}, + ValidCertificates: []CertificateInfo{latestValidCertInfo}, + InvalidCertificates: []CertificateInfo{invalidCertInfo}, DuplicatedCertificates: nil, }, }, { name: "2 valid, duplicated certs", - certificateInfos: []CertificateInfoModel{latestValidCertInfo, earlierValidCertInfo, invalidCertInfo}, + certificateInfos: []CertificateInfo{latestValidCertInfo, earlierValidCertInfo, invalidCertInfo}, want: ValidCertificateInfo{ - ValidCertificates: []CertificateInfoModel{latestValidCertInfo}, - InvalidCertificates: []CertificateInfoModel{invalidCertInfo}, - DuplicatedCertificates: []CertificateInfoModel{earlierValidCertInfo}, + ValidCertificates: []CertificateInfo{latestValidCertInfo}, + InvalidCertificates: []CertificateInfo{invalidCertInfo}, + DuplicatedCertificates: []CertificateInfo{earlierValidCertInfo}, }, }, } diff --git a/codesign/codesign.go b/codesign/codesign.go index 929f8c3a..361ff69d 100644 --- a/codesign/codesign.go +++ b/codesign/codesign.go @@ -292,7 +292,7 @@ func (m *Manager) selectCodeSigningStrategy(credentials devportalservice.Credent return codeSigningXcode, "Automatically managed signing is enabled in Xcode for the project.", nil } -func (m *Manager) downloadCertificates() ([]certificateutil.CertificateInfoModel, error) { +func (m *Manager) downloadCertificates() ([]certificateutil.CertificateInfo, error) { certificates, err := m.certDownloader.GetCertificates() if err != nil { return nil, fmt.Errorf("failed to download certificates: %s", err) @@ -312,7 +312,7 @@ func (m *Manager) downloadCertificates() ([]certificateutil.CertificateInfoModel return certificates, nil } -func (m *Manager) installCertificates(certificates []certificateutil.CertificateInfoModel) error { +func (m *Manager) installCertificates(certificates []certificateutil.CertificateInfo) error { for _, cert := range certificates { // Empty passphrase provided, as already parsed certificate + private key if err := m.assetInstaller.InstallCertificate(cert); err != nil { @@ -323,7 +323,7 @@ func (m *Manager) installCertificates(certificates []certificateutil.Certificate return nil } -func (m *Manager) validateCertificatesForXcodeManagedSigning(certificates []certificateutil.CertificateInfoModel) error { +func (m *Manager) validateCertificatesForXcodeManagedSigning(certificates []certificateutil.CertificateInfo) error { typeToLocalCerts, err := autocodesign.GetValidLocalCertificates(certificates) if err != nil { return err @@ -438,7 +438,7 @@ func (m *Manager) prepareAutomaticAssets(credentials devportalservice.Credential return codesignAssets, nil } -func (m *Manager) prepareManualAssets(certificates []certificateutil.CertificateInfoModel) error { +func (m *Manager) prepareManualAssets(certificates []certificateutil.CertificateInfo) error { if err := m.installCertificates(certificates); err != nil { return err } diff --git a/codesign/codesign_test.go b/codesign/codesign_test.go index fee1784e..ba2b0363 100644 --- a/codesign/codesign_test.go +++ b/codesign/codesign_test.go @@ -132,19 +132,19 @@ func TestManager_checkXcodeManagedCertificates(t *testing.T) { tests := []struct { name string distributionMethod autocodesign.DistributionType - certificates []certificateutil.CertificateInfoModel + certificates []certificateutil.CertificateInfo wantErr bool }{ { name: "no certs uploaded, development", distributionMethod: autocodesign.Development, - certificates: []certificateutil.CertificateInfoModel{}, + certificates: []certificateutil.CertificateInfo{}, wantErr: true, }, { name: "development, no matching cert", distributionMethod: autocodesign.Development, - certificates: []certificateutil.CertificateInfoModel{ + certificates: []certificateutil.CertificateInfo{ distCert, }, wantErr: true, @@ -152,19 +152,19 @@ func TestManager_checkXcodeManagedCertificates(t *testing.T) { { name: "no certs uploaded, distribution", distributionMethod: autocodesign.AppStore, - certificates: []certificateutil.CertificateInfoModel{}, + certificates: []certificateutil.CertificateInfo{}, }, { name: "1 certs uploaded, development", distributionMethod: autocodesign.Development, - certificates: []certificateutil.CertificateInfoModel{ + certificates: []certificateutil.CertificateInfo{ devCert, }, }, { name: "1 certs uploaded, distribution", distributionMethod: autocodesign.AdHoc, - certificates: []certificateutil.CertificateInfoModel{ + certificates: []certificateutil.CertificateInfo{ distCert, }, }, @@ -185,7 +185,7 @@ func TestManager_checkXcodeManagedCertificates(t *testing.T) { } } -func generateCert(t *testing.T, commonName string) certificateutil.CertificateInfoModel { +func generateCert(t *testing.T, commonName string) certificateutil.CertificateInfo { const ( teamID = "MYTEAMID" teamName = "BITFALL FEJLESZTO KORLATOLT FELELOSSEGU TARSASAG" diff --git a/export/codesing_group.go b/export/codesing_group.go index 920e9d18..669eee51 100644 --- a/export/codesing_group.go +++ b/export/codesing_group.go @@ -7,7 +7,7 @@ import ( // CodeSignGroup ... type CodeSignGroup interface { - Certificate() certificateutil.CertificateInfoModel - InstallerCertificate() *certificateutil.CertificateInfoModel + Certificate() certificateutil.CertificateInfo + InstallerCertificate() *certificateutil.CertificateInfo BundleIDProfileMap() map[string]profileutil.ProvisioningProfileInfoModel } diff --git a/export/export.go b/export/export.go index d42c2868..c8c4042f 100644 --- a/export/export.go +++ b/export/export.go @@ -13,7 +13,7 @@ import ( // SelectableCodeSignGroup ... type SelectableCodeSignGroup struct { - Certificate certificateutil.CertificateInfoModel + Certificate certificateutil.CertificateInfo BundleIDProfilesMap map[string][]profileutil.ProvisioningProfileInfoModel } @@ -42,7 +42,7 @@ func (group SelectableCodeSignGroup) String() string { return string(data) } -func isCertificateInstalled(installedCertificates []certificateutil.CertificateInfoModel, certificate certificateutil.CertificateInfoModel) bool { +func isCertificateInstalled(installedCertificates []certificateutil.CertificateInfo, certificate certificateutil.CertificateInfo) bool { for _, cert := range installedCertificates { if cert.Serial == certificate.Serial { return true @@ -52,11 +52,11 @@ func isCertificateInstalled(installedCertificates []certificateutil.CertificateI } // CreateSelectableCodeSignGroups ... -func CreateSelectableCodeSignGroups(certificates []certificateutil.CertificateInfoModel, profiles []profileutil.ProvisioningProfileInfoModel, bundleIDs []string) []SelectableCodeSignGroup { +func CreateSelectableCodeSignGroups(certificates []certificateutil.CertificateInfo, profiles []profileutil.ProvisioningProfileInfoModel, bundleIDs []string) []SelectableCodeSignGroup { groups := []SelectableCodeSignGroup{} serialProfilesMap := map[string][]profileutil.ProvisioningProfileInfoModel{} - serialCertificateMap := map[string]certificateutil.CertificateInfoModel{} + serialCertificateMap := map[string]certificateutil.CertificateInfo{} for _, profile := range profiles { for _, certificate := range profile.DeveloperCertificates { if !isCertificateInstalled(certificates, certificate) { diff --git a/export/ios.go b/export/ios.go index c97bf03e..69ab71e8 100644 --- a/export/ios.go +++ b/export/ios.go @@ -10,17 +10,17 @@ import ( // IosCodeSignGroup ... type IosCodeSignGroup struct { - certificate certificateutil.CertificateInfoModel + certificate certificateutil.CertificateInfo bundleIDProfileMap map[string]profileutil.ProvisioningProfileInfoModel } // Certificate ... -func (signGroup *IosCodeSignGroup) Certificate() certificateutil.CertificateInfoModel { +func (signGroup *IosCodeSignGroup) Certificate() certificateutil.CertificateInfo { return signGroup.certificate } // InstallerCertificate ... -func (signGroup *IosCodeSignGroup) InstallerCertificate() *certificateutil.CertificateInfoModel { +func (signGroup *IosCodeSignGroup) InstallerCertificate() *certificateutil.CertificateInfo { return nil } @@ -30,7 +30,7 @@ func (signGroup *IosCodeSignGroup) BundleIDProfileMap() map[string]profileutil.P } // NewIOSGroup ... -func NewIOSGroup(certificate certificateutil.CertificateInfoModel, bundleIDProfileMap map[string]profileutil.ProvisioningProfileInfoModel) *IosCodeSignGroup { +func NewIOSGroup(certificate certificateutil.CertificateInfo, bundleIDProfileMap map[string]profileutil.ProvisioningProfileInfoModel) *IosCodeSignGroup { return &IosCodeSignGroup{ certificate: certificate, bundleIDProfileMap: bundleIDProfileMap, diff --git a/export/mac.go b/export/mac.go index 73134a48..1f935bd1 100644 --- a/export/mac.go +++ b/export/mac.go @@ -8,18 +8,18 @@ import ( // MacCodeSignGroup ... type MacCodeSignGroup struct { - certificate certificateutil.CertificateInfoModel - installerCertificate *certificateutil.CertificateInfoModel + certificate certificateutil.CertificateInfo + installerCertificate *certificateutil.CertificateInfo bundleIDProfileMap map[string]profileutil.ProvisioningProfileInfoModel } // Certificate ... -func (signGroup *MacCodeSignGroup) Certificate() certificateutil.CertificateInfoModel { +func (signGroup *MacCodeSignGroup) Certificate() certificateutil.CertificateInfo { return signGroup.certificate } // InstallerCertificate ... -func (signGroup *MacCodeSignGroup) InstallerCertificate() *certificateutil.CertificateInfoModel { +func (signGroup *MacCodeSignGroup) InstallerCertificate() *certificateutil.CertificateInfo { return signGroup.installerCertificate } @@ -29,7 +29,7 @@ func (signGroup *MacCodeSignGroup) BundleIDProfileMap() map[string]profileutil.P } // NewMacGroup ... -func NewMacGroup(certificate certificateutil.CertificateInfoModel, installerCertificate *certificateutil.CertificateInfoModel, bundleIDProfileMap map[string]profileutil.ProvisioningProfileInfoModel) *MacCodeSignGroup { +func NewMacGroup(certificate certificateutil.CertificateInfo, installerCertificate *certificateutil.CertificateInfo, bundleIDProfileMap map[string]profileutil.ProvisioningProfileInfoModel) *MacCodeSignGroup { return &MacCodeSignGroup{ certificate: certificate, installerCertificate: installerCertificate, @@ -38,14 +38,14 @@ func NewMacGroup(certificate certificateutil.CertificateInfoModel, installerCert } // CreateMacCodeSignGroup ... -func CreateMacCodeSignGroup(selectableGroups []SelectableCodeSignGroup, installedInstallerCertificates []certificateutil.CertificateInfoModel, exportMethod exportoptions.Method) []MacCodeSignGroup { +func CreateMacCodeSignGroup(selectableGroups []SelectableCodeSignGroup, installedInstallerCertificates []certificateutil.CertificateInfo, exportMethod exportoptions.Method) []MacCodeSignGroup { macosCodeSignGroups := []MacCodeSignGroup{} iosCodesignGroups := CreateIosCodeSignGroups(selectableGroups) for _, group := range iosCodesignGroups { if exportMethod.IsAppStore() { - installerCertificates := []certificateutil.CertificateInfoModel{} + installerCertificates := []certificateutil.CertificateInfo{} for _, installerCertificate := range installedInstallerCertificates { if installerCertificate.TeamID == group.certificate.TeamID { diff --git a/exportoptionsgenerator/certificates.go b/exportoptionsgenerator/certificates.go index 5b277e14..31ae6eb1 100644 --- a/exportoptionsgenerator/certificates.go +++ b/exportoptionsgenerator/certificates.go @@ -4,14 +4,14 @@ import "github.com/bitrise-io/go-xcode/v2/certificateutil" // CodesignIdentityProvider can list certificate infos. type CodesignIdentityProvider interface { - ListCodesignIdentities() ([]certificateutil.CertificateInfoModel, error) + ListCodesignIdentities() ([]certificateutil.CertificateInfo, error) } // LocalCodesignIdentityProvider ... type LocalCodesignIdentityProvider struct{} // ListCodesignIdentities ... -func (p LocalCodesignIdentityProvider) ListCodesignIdentities() ([]certificateutil.CertificateInfoModel, error) { +func (p LocalCodesignIdentityProvider) ListCodesignIdentities() ([]certificateutil.CertificateInfo, error) { certs, err := certificateutil.InstalledCodesigningCertificateInfos() if err != nil { return nil, err diff --git a/exportoptionsgenerator/exportoptionsgenerator_test.go b/exportoptionsgenerator/exportoptionsgenerator_test.go index ac7266b2..92414e0d 100644 --- a/exportoptionsgenerator/exportoptionsgenerator_test.go +++ b/exportoptionsgenerator/exportoptionsgenerator_test.go @@ -374,7 +374,7 @@ func TestExportOptionsGenerator_GenerateApplicationExportOptions(t *testing.T) { teamID = "TEAM123" ) - certificate := certificateutil.CertificateInfoModel{Serial: "serial", CommonName: "Development Certificate", TeamID: teamID} + certificate := certificateutil.CertificateInfo{Serial: "serial", CommonName: "Development Certificate", TeamID: teamID} tests := []struct { name string @@ -422,21 +422,21 @@ func TestExportOptionsGenerator_GenerateApplicationExportOptions(t *testing.T) { g := New(newXcodeVersionReader(t, tt.xcodeVersion), logger) g.certificateProvider = MockCodesignIdentityProvider{ - []certificateutil.CertificateInfoModel{certificate}, + []certificateutil.CertificateInfo{certificate}, } profile := profileutil.ProvisioningProfileInfoModel{ BundleID: bundleID, TeamID: teamID, ExportType: tt.exportMethod, Name: "Development Application Profile", - DeveloperCertificates: []certificateutil.CertificateInfoModel{certificate}, + DeveloperCertificates: []certificateutil.CertificateInfo{certificate}, } profileForClip := profileutil.ProvisioningProfileInfoModel{ BundleID: bundleIDClip, TeamID: teamID, ExportType: tt.exportMethod, Name: "Development App Clip Profile", - DeveloperCertificates: []certificateutil.CertificateInfoModel{certificate}, + DeveloperCertificates: []certificateutil.CertificateInfo{certificate}, } g.profileProvider = MockProvisioningProfileProvider{ []profileutil.ProvisioningProfileInfoModel{ @@ -483,7 +483,7 @@ func TestExportOptionsGenerator_GenerateApplicationExportOptions_WhenNoProfileFo teamID = "TEAM123" ) - certificate := certificateutil.CertificateInfoModel{Serial: "serial", CommonName: "Development Certificate", TeamID: teamID} + certificate := certificateutil.CertificateInfo{Serial: "serial", CommonName: "Development Certificate", TeamID: teamID} tests := []struct { name string @@ -534,7 +534,7 @@ func TestExportOptionsGenerator_GenerateApplicationExportOptions_WhenNoProfileFo g := New(xcodeVersionReader, logger) g.certificateProvider = MockCodesignIdentityProvider{ - []certificateutil.CertificateInfoModel{certificate}, + []certificateutil.CertificateInfo{certificate}, } g.profileProvider = MockProvisioningProfileProvider{} @@ -570,10 +570,10 @@ func TestExportOptionsGenerator_GenerateApplicationExportOptions_WhenNoProfileFo } type MockCodesignIdentityProvider struct { - codesignIdentities []certificateutil.CertificateInfoModel + codesignIdentities []certificateutil.CertificateInfo } -func (p MockCodesignIdentityProvider) ListCodesignIdentities() ([]certificateutil.CertificateInfoModel, error) { +func (p MockCodesignIdentityProvider) ListCodesignIdentities() ([]certificateutil.CertificateInfo, error) { return p.codesignIdentities, nil } diff --git a/profileutil/info_model.go b/profileutil/info_model.go index ac77adf3..819d966e 100644 --- a/profileutil/info_model.go +++ b/profileutil/info_model.go @@ -26,7 +26,7 @@ type ProvisioningProfileInfoModel struct { BundleID string ExportType exportoptions.Method ProvisionedDevices []string - DeveloperCertificates []certificateutil.CertificateInfoModel + DeveloperCertificates []certificateutil.CertificateInfo CreationDate time.Time ExpirationDate time.Time Entitlements plistutil.MapData @@ -48,7 +48,7 @@ func collectCapabilitesPrintableInfo(entitlements plistutil.MapData) map[string] } // PrintableProvisioningProfileInfo ... -func (info ProvisioningProfileInfoModel) String(installedCertificates ...certificateutil.CertificateInfoModel) string { +func (info ProvisioningProfileInfoModel) String(installedCertificates ...certificateutil.CertificateInfo) string { printable := map[string]interface{}{} printable["name"] = fmt.Sprintf("%s (%s)", info.Name, info.UUID) printable["export_type"] = string(info.ExportType) @@ -125,7 +125,7 @@ func (info ProvisioningProfileInfoModel) CheckValidity() error { } // HasInstalledCertificate ... -func (info ProvisioningProfileInfoModel) HasInstalledCertificate(installedCertificates []certificateutil.CertificateInfoModel) bool { +func (info ProvisioningProfileInfoModel) HasInstalledCertificate(installedCertificates []certificateutil.CertificateInfo) bool { has := false for _, certificate := range info.DeveloperCertificates { for _, installedCertificate := range installedCertificates { From c298d85a1209e3a1d4fc4730b47c38202cc48838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krisztia=CC=81n=20Go=CC=88drei?= Date: Fri, 20 Jun 2025 17:03:00 +0200 Subject: [PATCH 7/7] Inverse certificateutil dependencies --- autocodesign/autocodesign_test.go | 47 ++++++------ .../certdownloader/certdownloader_test.go | 5 +- autocodesign/certificates.go | 9 ++- autocodesign/certificates_test.go | 21 +++--- .../devportalclient/spaceship/certificates.go | 2 +- autocodesign/example_test.go | 3 +- autocodesign/keychain/keychain_test.go | 5 +- .../localcodesignasset_test.go | 57 +++++++-------- .../localcodesignasset/profilelookup_test.go | 36 +++++---- certificateutil/info_model.go | 14 ++-- certificateutil/security_tool.go | 73 +++++++++---------- certificateutil/util.go | 42 ++++++----- certificateutil/util_test.go | 16 ++-- codesign/codesign.go | 10 ++- codesign/codesign_test.go | 5 +- exportoptionsgenerator/certificates.go | 23 +++++- timeutil/time_provider.go | 17 +++++ 17 files changed, 222 insertions(+), 163 deletions(-) create mode 100644 timeutil/time_provider.go diff --git a/autocodesign/autocodesign_test.go b/autocodesign/autocodesign_test.go index ebf46232..56dae8a0 100644 --- a/autocodesign/autocodesign_test.go +++ b/autocodesign/autocodesign_test.go @@ -62,22 +62,16 @@ func newMockProfile(m profileArgs) Profile { return profile } -func newCertificate(t *testing.T, teamID, teamName, commonName string, expiry time.Time) certificateutil.CertificateInfo { - cert, privateKey, err := certificateutil.GenerateTestCertificate(int64(1), teamID, teamName, commonName, expiry) - if err != nil { - t.Fatalf("init: failed to generate certificate: %s", err) - } - return certificateutil.NewCertificateInfo(*cert, privateKey) -} - func Test_codesignAssetManager_EnsureCodesignAssets(t *testing.T) { log.SetEnableDebugLog(true) const teamID = "MYTEAMID" const commonNameIOSDevelopment = "Apple Development: test" const teamName = "BITFALL FEJLESZTO KORLATOLT FELELOSSEGU TARSASAG" - expiry := time.Now().AddDate(1, 0, 0) - devCert := newCertificate(t, teamID, teamName, commonNameIOSDevelopment, expiry) + notBefore := time.Now() + expiry := notBefore.AddDate(1, 0, 0) + devCert, err := certificateutil.GenerateTestCertificateInfo(1, teamID, teamName, commonNameIOSDevelopment, notBefore, expiry) + require.NoError(t, err) t.Logf("Test certificate generated. %s", devCert) @@ -287,8 +281,10 @@ func Test_codesignAssetManager_EnsureCodesignAssets(t *testing.T) { func Test_GivenNoValidAppID_WhenEnsureAppClipProfile_ThenItFails(t *testing.T) { // Given const teamID = "MY_TEAM_ID" - expiry := time.Now().AddDate(1, 0, 0) - devCert := newCertificate(t, teamID, "MY_TEAM", "Apple Development: test", expiry) + notBefore := time.Now() + expiry := notBefore.AddDate(1, 0, 0) + devCert, err := certificateutil.GenerateTestCertificateInfo(1, teamID, "MY_TEAM", "Apple Development: test", notBefore, expiry) + require.NoError(t, err) client := newClientWithoutAppIDAndProfile(devCert) assetWriter := newDefaultMockAssetWriter() @@ -311,7 +307,7 @@ func Test_GivenNoValidAppID_WhenEnsureAppClipProfile_ThenItFails(t *testing.T) { } // When - _, err := manager.EnsureCodesignAssets(appLayout, opts) + _, err = manager.EnsureCodesignAssets(appLayout, opts) // Then require.ErrorAs(t, err, &ErrAppClipAppID{}) @@ -322,8 +318,10 @@ func Test_GivenAppIDWithoutAppleSignIn_WhenEnsureAppClipProfile_ThenItFails(t *t const teamID = "MY_TEAM_ID" const appClipBundleID = "io.bitrise.appclip" - expiry := time.Now().AddDate(1, 0, 0) - devCert := newCertificate(t, teamID, "MY_TEAM", "Apple Development: test", expiry) + notBefore := time.Now() + expiry := notBefore.AddDate(1, 0, 0) + devCert, err := certificateutil.GenerateTestCertificateInfo(1, teamID, "MY_TEAM", "Apple Development: test", notBefore, expiry) + require.NoError(t, err) client := newClientWithAppIDWithoutAppleSignIn(devCert, appClipBundleID) assetWriter := newDefaultMockAssetWriter() @@ -349,7 +347,7 @@ func Test_GivenAppIDWithoutAppleSignIn_WhenEnsureAppClipProfile_ThenItFails(t *t } // When - _, err := manager.EnsureCodesignAssets(appLayout, opts) + _, err = manager.EnsureCodesignAssets(appLayout, opts) // Then require.ErrorAs(t, err, &ErrAppClipAppIDWithAppleSigning{}) @@ -358,8 +356,10 @@ func Test_GivenAppIDWithoutAppleSignIn_WhenEnsureAppClipProfile_ThenItFails(t *t func Test_GivenProfileExpired_WhenProfilesInconsistent_ThenItRetries(t *testing.T) { // Given const teamID = "MY_TEAM_ID" - expiry := time.Now().AddDate(1, 0, 0) - devCert := newCertificate(t, teamID, "MY_TEAM", "Apple Development: test", expiry) + notBefore := time.Now() + expiry := notBefore.AddDate(1, 0, 0) + devCert, err := certificateutil.GenerateTestCertificateInfo(1, teamID, "MY_TEAM", "Apple Development: test", notBefore, expiry) + require.NoError(t, err) expiredProfile := newMockProfile(profileArgs{ attributes: appstoreconnect.ProfileAttributes{ @@ -423,7 +423,7 @@ func Test_GivenProfileExpired_WhenProfilesInconsistent_ThenItRetries(t *testing. } // When - _, err := manager.EnsureCodesignAssets(appLayout, opts) + _, err = manager.EnsureCodesignAssets(appLayout, opts) // Then require.NoError(t, err) @@ -432,9 +432,12 @@ func Test_GivenProfileExpired_WhenProfilesInconsistent_ThenItRetries(t *testing. func Test_GivenLocalProfile_WhenCertificateIsMissing_ThenInstalled(t *testing.T) { // Given const teamID = "MY_TEAM_ID" - expiry := time.Now().AddDate(1, 0, 0) - devCert1 := newCertificate(t, teamID, "MY_TEAM", "Apple Development: test 1", expiry) - devCert2 := newCertificate(t, teamID, "MY_TEAM", "Apple Development: test 2", expiry) + notBefore := time.Now() + expiry := notBefore.AddDate(1, 0, 0) + devCert1, err := certificateutil.GenerateTestCertificateInfo(1, teamID, "MY_TEAM", "Apple Development: test 1", notBefore, expiry) + require.NoError(t, err) + devCert2, err := certificateutil.GenerateTestCertificateInfo(1, teamID, "MY_TEAM", "Apple Development: test 2", notBefore, expiry) + require.NoError(t, err) validProfile := newMockProfile(profileArgs{ attributes: appstoreconnect.ProfileAttributes{ diff --git a/autocodesign/certdownloader/certdownloader_test.go b/autocodesign/certdownloader/certdownloader_test.go index 371b0e0f..b5e4007e 100644 --- a/autocodesign/certdownloader/certdownloader_test.go +++ b/autocodesign/certdownloader/certdownloader_test.go @@ -97,10 +97,11 @@ func createTestCert(t *testing.T) certificateutil.CertificateInfo { commonName = "Apple Developer: test" teamName = "BITFALL FEJLESZTO KORLATOLT FELELOSSEGU TARSASAG" ) - expiry := time.Now().AddDate(1, 0, 0) + notBefore := time.Now() + expiry := notBefore.AddDate(1, 0, 0) serial := int64(1234) - cert, privateKey, err := certificateutil.GenerateTestCertificate(serial, teamID, teamName, commonName, expiry) + cert, privateKey, err := certificateutil.GenerateTestCertificate(serial, teamID, teamName, commonName, notBefore, expiry) if err != nil { t.Errorf("init: failed to generate certificate: %s", err) } diff --git a/autocodesign/certificates.go b/autocodesign/certificates.go index 0fdb6f13..15106a30 100644 --- a/autocodesign/certificates.go +++ b/autocodesign/certificates.go @@ -7,6 +7,7 @@ import ( "github.com/bitrise-io/go-utils/log" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/timeutil" ) func selectCertificatesAndDistributionTypes(certificateSource DevPortalClient, typeToLocalCerts LocalCertificates, distribution DistributionType, signUITestTargets bool, verboseLog bool) (map[appstoreconnect.CertificateType][]Certificate, []DistributionType, error) { @@ -90,8 +91,8 @@ func getValidCertificates(typeToLocalCerts LocalCertificates, client DevPortalCl } // GetValidLocalCertificates returns validated and deduplicated local certificates -func GetValidLocalCertificates(certificates []certificateutil.CertificateInfo) (LocalCertificates, error) { - preFilteredCerts := certificateutil.FilterValidCertificateInfos(certificates) +func GetValidLocalCertificates(certificates []certificateutil.CertificateInfo, timeProvider timeutil.TimeProvider) (LocalCertificates, error) { + preFilteredCerts := certificateutil.FilterValidCertificateInfos(certificates, timeProvider) if len(preFilteredCerts.InvalidCertificates) != 0 { log.Warnf("Ignoring expired or not yet valid certificates: %s", preFilteredCerts.InvalidCertificates) @@ -104,7 +105,7 @@ func GetValidLocalCertificates(certificates []certificateutil.CertificateInfo) ( localCertificates := LocalCertificates{} for _, certType := range []appstoreconnect.CertificateType{appstoreconnect.IOSDevelopment, appstoreconnect.IOSDistribution} { - localCertificates[certType] = filterCertificates(preFilteredCerts.ValidCertificates, certType) + localCertificates[certType] = filterCertificates(preFilteredCerts.ValidCertificates, certType, timeProvider) } log.Debugf("Valid and deduplicated certificates:\n%s", certsToString(preFilteredCerts.ValidCertificates)) @@ -150,7 +151,7 @@ func logAllAPICertificates(client DevPortalClient) error { } // filterCertificates returns the certificates matching to the given common name, developer team ID, and distribution type. -func filterCertificates(certificates []certificateutil.CertificateInfo, certificateType appstoreconnect.CertificateType) []certificateutil.CertificateInfo { +func filterCertificates(certificates []certificateutil.CertificateInfo, certificateType appstoreconnect.CertificateType, timeProvider timeutil.TimeProvider) []certificateutil.CertificateInfo { // filter by distribution type var filteredCertificates []certificateutil.CertificateInfo for _, certificate := range certificates { diff --git a/autocodesign/certificates_test.go b/autocodesign/certificates_test.go index dfe32c20..ae80175a 100644 --- a/autocodesign/certificates_test.go +++ b/autocodesign/certificates_test.go @@ -10,6 +10,7 @@ import ( "github.com/bitrise-io/go-utils/log" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/timeutil" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -101,19 +102,20 @@ func Test_getValidCertificates(t *testing.T) { teamID = "MYTEAMID" teamName = "BITFALL FEJLESZTO KORLATOLT FELELOSSEGU TARSASAG" ) - expiry := time.Now().AddDate(1, 0, 0) + notBefore := time.Now() + expiry := notBefore.AddDate(1, 0, 0) - cert, privateKey, err := certificateutil.GenerateTestCertificate(int64(1), teamID, teamName, "Apple Development: test", expiry) + cert, privateKey, err := certificateutil.GenerateTestCertificate(int64(1), teamID, teamName, "Apple Development: test", notBefore, expiry) require.NoError(t, err, "init: failed to generate certificate: %s", err) devCert := certificateutil.NewCertificateInfo(*cert, privateKey) t.Logf("Test certificate generated. %s", devCert) - cert, privateKey, err = certificateutil.GenerateTestCertificate(int64(2), teamID, teamName, "iPhone Developer: test2", expiry) + cert, privateKey, err = certificateutil.GenerateTestCertificate(int64(2), teamID, teamName, "iPhone Developer: test2", notBefore, expiry) require.NoError(t, err, "init: failed to generate certificate: %s", err) devCert2 := certificateutil.NewCertificateInfo(*cert, privateKey) t.Logf("Test certificate generated. %s", devCert2) - distCert, privateKey, err := certificateutil.GenerateTestCertificate(int64(10), teamID, teamName, "Apple Distribution: test", expiry) + distCert, privateKey, err := certificateutil.GenerateTestCertificate(int64(10), teamID, teamName, "Apple Distribution: test", notBefore, expiry) require.NoError(t, err, "init: failed to generate certificate: %s", err) distributionCert := certificateutil.NewCertificateInfo(*distCert, privateKey) t.Logf("Test certificate generated. %s", distributionCert) @@ -345,19 +347,20 @@ func TestGetValidLocalCertificates(t *testing.T) { teamID = "MYTEAMID" teamName = "BITFALL FEJLESZTO KORLATOLT FELELOSSEGU TARSASAG" ) - expiry := time.Now().AddDate(1, 0, 0) + notBefore := time.Now() + expiry := notBefore.AddDate(1, 0, 0) - cert, privateKey, err := certificateutil.GenerateTestCertificate(int64(1), teamID, teamName, "Apple Development: test", expiry) + cert, privateKey, err := certificateutil.GenerateTestCertificate(int64(1), teamID, teamName, "Apple Development: test", notBefore, expiry) require.NoError(t, err, "init: failed to generate certificate: %s", err) devCert := certificateutil.NewCertificateInfo(*cert, privateKey) t.Logf("Test certificate generated. %s", devCert) - cert, privateKey, err = certificateutil.GenerateTestCertificate(int64(2), teamID, teamName, "iPhone Developer: test2", expiry) + cert, privateKey, err = certificateutil.GenerateTestCertificate(int64(2), teamID, teamName, "iPhone Developer: test2", notBefore, expiry) require.NoError(t, err, "init: failed to generate certificate: %s", err) devCert2 := certificateutil.NewCertificateInfo(*cert, privateKey) t.Logf("Test certificate generated. %s", devCert2) - distCert, privateKey, err := certificateutil.GenerateTestCertificate(int64(10), teamID, teamName, "Apple Distribution: test", expiry) + distCert, privateKey, err := certificateutil.GenerateTestCertificate(int64(10), teamID, teamName, "Apple Distribution: test", notBefore, expiry) require.NoError(t, err, "init: failed to generate certificate: %s", err) distributionCert := certificateutil.NewCertificateInfo(*distCert, privateKey) t.Logf("Test certificate generated. %s", distributionCert) @@ -401,7 +404,7 @@ func TestGetValidLocalCertificates(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := GetValidLocalCertificates(tt.certificates) + got, err := GetValidLocalCertificates(tt.certificates, timeutil.NewDefaultTimeProvider()) require.NoError(t, err) for _, certType := range []appstoreconnect.CertificateType{appstoreconnect.IOSDevelopment, appstoreconnect.IOSDistribution} { diff --git a/autocodesign/devportalclient/spaceship/certificates.go b/autocodesign/devportalclient/spaceship/certificates.go index 73d1d3d8..6f36cc3b 100644 --- a/autocodesign/devportalclient/spaceship/certificates.go +++ b/autocodesign/devportalclient/spaceship/certificates.go @@ -107,7 +107,7 @@ func (s *CertificateSource) getCertificates(devCerts bool) ([]autocodesign.Certi return nil, err } - cert, err := certificateutil.CeritifcateFromPemContent(pemContent) + cert, err := certificateutil.CertificateFromPemContent(pemContent) if err != nil { return nil, err } diff --git a/autocodesign/example_test.go b/autocodesign/example_test.go index 80f96249..f1778ac2 100644 --- a/autocodesign/example_test.go +++ b/autocodesign/example_test.go @@ -18,6 +18,7 @@ import ( "github.com/bitrise-io/go-xcode/v2/autocodesign/projectmanager" "github.com/bitrise-io/go-xcode/v2/codesign" "github.com/bitrise-io/go-xcode/v2/devportalservice" + "github.com/bitrise-io/go-xcode/v2/timeutil" ) type config struct { @@ -82,7 +83,7 @@ func Example() { panic(fmt.Errorf("failed to download certificates: %w", err)) } - typeToLocalCerts, err := autocodesign.GetValidLocalCertificates(certs) + typeToLocalCerts, err := autocodesign.GetValidLocalCertificates(certs, timeutil.NewDefaultTimeProvider()) if err != nil { panic(err) } diff --git a/autocodesign/keychain/keychain_test.go b/autocodesign/keychain/keychain_test.go index 0c5547fb..7cbef365 100644 --- a/autocodesign/keychain/keychain_test.go +++ b/autocodesign/keychain/keychain_test.go @@ -66,9 +66,10 @@ func TestKeychain_importCertificate(t *testing.T) { const teamID = "MYTEAMID" const commonNameIOSDevelopment = "iPhone Developer: test" const teamName = "BITFALL FEJLESZTO KORLATOLT FELELOSSEGU TARSASAG" - expiry := time.Now().AddDate(1, 0, 0) + notBefore := time.Now() + expiry := notBefore.AddDate(1, 0, 0) - cert, privateKey, err := certificateutil.GenerateTestCertificate(int64(1), teamID, teamName, commonNameIOSDevelopment, expiry) + cert, privateKey, err := certificateutil.GenerateTestCertificate(int64(1), teamID, teamName, commonNameIOSDevelopment, notBefore, expiry) if err != nil { t.Fatalf("init: failed to generate certificate: %s", err) } diff --git a/autocodesign/localcodesignasset/localcodesignasset_test.go b/autocodesign/localcodesignasset/localcodesignasset_test.go index ba1668d2..3a7612a1 100644 --- a/autocodesign/localcodesignasset/localcodesignasset_test.go +++ b/autocodesign/localcodesignasset/localcodesignasset_test.go @@ -5,6 +5,7 @@ import ( "time" devportaltime "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/time" + "github.com/stretchr/testify/require" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/appstoreconnect" @@ -220,6 +221,7 @@ func profileFromModel(profileInfo profileutil.ProvisioningProfileInfoModel) auto } func profiles(t *testing.T) []profileutil.ProvisioningProfileInfoModel { + now := time.Now() iosDevProfile := profileutil.ProvisioningProfileInfoModel{ UUID: "uuid-1", Name: "Valid development profile", @@ -228,9 +230,9 @@ func profiles(t *testing.T) []profileutil.ProvisioningProfileInfoModel { BundleID: "io.ios.valid", ExportType: exportoptions.MethodDevelopment, ProvisionedDevices: []string{"device-1", "device-2", "device-3"}, - DeveloperCertificates: []certificateutil.CertificateInfo{devCert(t, dateRelativeToNow(1, 0, 0))}, - CreationDate: dateRelativeToNow(0, -1, 0), - ExpirationDate: dateRelativeToNow(0, 1, 0), + DeveloperCertificates: []certificateutil.CertificateInfo{devCert(t, now, now.AddDate(1, 0, 0))}, + CreationDate: now.AddDate(0, -1, 0), + ExpirationDate: now.AddDate(0, 1, 0), Entitlements: entitlements(), ProvisionsAllDevices: false, Type: profileutil.ProfileTypeIos, @@ -243,9 +245,9 @@ func profiles(t *testing.T) []profileutil.ProvisioningProfileInfoModel { BundleID: "io.tvos.valid", ExportType: exportoptions.MethodAppStore, ProvisionedDevices: nil, - DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, dateRelativeToNow(1, 0, 0))}, - CreationDate: dateRelativeToNow(0, -1, 0), - ExpirationDate: dateRelativeToNow(0, 1, 0), + DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, now, now.AddDate(1, 0, 0))}, + CreationDate: now.AddDate(0, -1, 0), + ExpirationDate: now.AddDate(0, 1, 0), Entitlements: nil, ProvisionsAllDevices: true, Type: profileutil.ProfileTypeTvOs, @@ -258,9 +260,9 @@ func profiles(t *testing.T) []profileutil.ProvisioningProfileInfoModel { BundleID: "io.tvos.expired", ExportType: exportoptions.MethodAppStore, ProvisionedDevices: nil, - DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, dateRelativeToNow(1, 0, 0))}, - CreationDate: dateRelativeToNow(0, -1, 0), - ExpirationDate: dateRelativeToNow(0, 0, -1), + DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, now, now.AddDate(1, 0, 0))}, + CreationDate: now.AddDate(0, -1, 0), + ExpirationDate: now.AddDate(0, 0, -1), Entitlements: nil, ProvisionsAllDevices: true, Type: profileutil.ProfileTypeIos, @@ -273,9 +275,9 @@ func profiles(t *testing.T) []profileutil.ProvisioningProfileInfoModel { BundleID: "io.ios.*", ExportType: exportoptions.MethodDevelopment, ProvisionedDevices: []string{"device-1", "device-2", "device-3"}, - DeveloperCertificates: []certificateutil.CertificateInfo{devCert(t, dateRelativeToNow(1, 0, 0))}, - CreationDate: dateRelativeToNow(0, -1, 0), - ExpirationDate: dateRelativeToNow(0, 1, 0), + DeveloperCertificates: []certificateutil.CertificateInfo{devCert(t, now, now.AddDate(1, 0, 0))}, + CreationDate: now.AddDate(0, -1, 0), + ExpirationDate: now.AddDate(0, 1, 0), Entitlements: entitlements(), ProvisionsAllDevices: false, Type: profileutil.ProfileTypeIos, @@ -294,13 +296,14 @@ func entitlements() map[string]interface{} { } func certsByType(t *testing.T) map[appstoreconnect.CertificateType][]autocodesign.Certificate { - expiry := dateRelativeToNow(1, 0, 0) + notBefore := time.Now() + expiry := notBefore.AddDate(1, 0, 0) devCert := autocodesign.Certificate{ - CertificateInfo: devCert(t, expiry), + CertificateInfo: devCert(t, notBefore, expiry), ID: "dev", } distCert := autocodesign.Certificate{ - CertificateInfo: distCert(t, expiry), + CertificateInfo: distCert(t, notBefore, expiry), ID: "dist", } @@ -310,22 +313,14 @@ func certsByType(t *testing.T) map[appstoreconnect.CertificateType][]autocodesig } } -func devCert(t *testing.T, expiry time.Time) certificateutil.CertificateInfo { - return newCertificate(t, 1, teamID, teamName, "Development certificate", expiry) +func devCert(t *testing.T, notBefore, expiry time.Time) certificateutil.CertificateInfo { + certInfo, err := certificateutil.GenerateTestCertificateInfo(1, teamID, teamName, "Development certificate", notBefore, expiry) + require.NoError(t, err) + return certInfo } -func distCert(t *testing.T, expiry time.Time) certificateutil.CertificateInfo { - return newCertificate(t, 2, teamID, teamName, "Distribution certificate", expiry) -} - -func newCertificate(t *testing.T, serial int, teamID, teamName, commonName string, expiry time.Time) certificateutil.CertificateInfo { - cert, privateKey, err := certificateutil.GenerateTestCertificate(int64(serial), teamID, teamName, commonName, expiry) - if err != nil { - t.Fatalf("init: failed to generate certificate: %s", err) - } - return certificateutil.NewCertificateInfo(*cert, privateKey) -} - -func dateRelativeToNow(years, months, days int) time.Time { - return time.Now().AddDate(years, months, days) +func distCert(t *testing.T, notBefore, expiry time.Time) certificateutil.CertificateInfo { + certInfo, err := certificateutil.GenerateTestCertificateInfo(1, teamID, teamName, "Distribution certificate", notBefore, expiry) + require.NoError(t, err) + return certInfo } diff --git a/autocodesign/localcodesignasset/profilelookup_test.go b/autocodesign/localcodesignasset/profilelookup_test.go index 10eaaa2b..94199bdb 100644 --- a/autocodesign/localcodesignasset/profilelookup_test.go +++ b/autocodesign/localcodesignasset/profilelookup_test.go @@ -2,6 +2,7 @@ package localcodesignasset import ( "testing" + "time" "github.com/bitrise-io/go-xcode/v2/autocodesign" "github.com/bitrise-io/go-xcode/v2/certificateutil" @@ -198,6 +199,7 @@ func Test_GivenProfiles_WhenFiltersForNonExisting_ThenItIsMissing(t *testing.T) // Helpers func iosXcodeManagedDevelopmentProfile(t *testing.T) profileutil.ProvisioningProfileInfoModel { + now := time.Now() return profileutil.ProvisioningProfileInfoModel{ UUID: "uuid-1", Name: "iOS Team Provisioning Profile: io.ios.managed", @@ -206,9 +208,9 @@ func iosXcodeManagedDevelopmentProfile(t *testing.T) profileutil.ProvisioningPro BundleID: "io.ios.managed", ExportType: exportoptions.MethodDevelopment, ProvisionedDevices: []string{"ios-device-1", "ios-device-2", "ios-device-3"}, - DeveloperCertificates: []certificateutil.CertificateInfo{devCert(t, dateRelativeToNow(1, 0, 0))}, - CreationDate: dateRelativeToNow(0, 0, -1), - ExpirationDate: dateRelativeToNow(0, 0, 5), + DeveloperCertificates: []certificateutil.CertificateInfo{devCert(t, now, now.AddDate(1, 0, 0))}, + CreationDate: now.AddDate(0, 0, -1), + ExpirationDate: now.AddDate(0, 0, 5), Entitlements: firstSetOfEntitlements(), ProvisionsAllDevices: false, Type: profileutil.ProfileTypeIos, @@ -216,6 +218,7 @@ func iosXcodeManagedDevelopmentProfile(t *testing.T) profileutil.ProvisioningPro } func iosDevelopmentProfile(t *testing.T) profileutil.ProvisioningProfileInfoModel { + now := time.Now() return profileutil.ProvisioningProfileInfoModel{ UUID: "uuid-1", Name: "iOS development profile", @@ -224,9 +227,9 @@ func iosDevelopmentProfile(t *testing.T) profileutil.ProvisioningProfileInfoMode BundleID: "io.ios", ExportType: exportoptions.MethodDevelopment, ProvisionedDevices: []string{"ios-device-1", "ios-device-2", "ios-device-3"}, - DeveloperCertificates: []certificateutil.CertificateInfo{devCert(t, dateRelativeToNow(1, 0, 0))}, - CreationDate: dateRelativeToNow(0, 0, -1), - ExpirationDate: dateRelativeToNow(0, 0, 5), + DeveloperCertificates: []certificateutil.CertificateInfo{devCert(t, now, now.AddDate(1, 0, 0))}, + CreationDate: now.AddDate(0, 0, -1), + ExpirationDate: now.AddDate(0, 0, 5), Entitlements: firstSetOfEntitlements(), ProvisionsAllDevices: false, Type: profileutil.ProfileTypeIos, @@ -234,6 +237,7 @@ func iosDevelopmentProfile(t *testing.T) profileutil.ProvisioningProfileInfoMode } func iosAppStoreProfile(t *testing.T) profileutil.ProvisioningProfileInfoModel { + now := time.Now() return profileutil.ProvisioningProfileInfoModel{ UUID: "uuid-2", Name: "iOS app store profile", @@ -242,9 +246,9 @@ func iosAppStoreProfile(t *testing.T) profileutil.ProvisioningProfileInfoModel { BundleID: "io.ios", ExportType: exportoptions.MethodAppStore, ProvisionedDevices: nil, - DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, dateRelativeToNow(1, 0, 0))}, - CreationDate: dateRelativeToNow(0, 0, -1), - ExpirationDate: dateRelativeToNow(0, 0, 5), + DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, now, now.AddDate(1, 0, 0))}, + CreationDate: now.AddDate(0, 0, -1), + ExpirationDate: now.AddDate(0, 0, 5), Entitlements: nil, ProvisionsAllDevices: true, Type: profileutil.ProfileTypeIos, @@ -252,6 +256,7 @@ func iosAppStoreProfile(t *testing.T) profileutil.ProvisioningProfileInfoModel { } func tvosAdHocProfile(t *testing.T) profileutil.ProvisioningProfileInfoModel { + now := time.Now() return profileutil.ProvisioningProfileInfoModel{ UUID: "uuid-3", Name: "tvOS ad hoc profile", @@ -260,9 +265,9 @@ func tvosAdHocProfile(t *testing.T) profileutil.ProvisioningProfileInfoModel { BundleID: "io.tvos", ExportType: exportoptions.MethodAdHoc, ProvisionedDevices: []string{"tvos-device-1", "tvos-device-2", "tvos-device-3"}, - DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, dateRelativeToNow(1, 0, 0))}, - CreationDate: dateRelativeToNow(0, 0, -1), - ExpirationDate: dateRelativeToNow(0, 0, 10), + DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, now, now.AddDate(1, 0, 0))}, + CreationDate: now.AddDate(0, 0, -1), + ExpirationDate: now.AddDate(0, 0, 10), Entitlements: nil, ProvisionsAllDevices: false, Type: profileutil.ProfileTypeTvOs, @@ -270,6 +275,7 @@ func tvosAdHocProfile(t *testing.T) profileutil.ProvisioningProfileInfoModel { } func tvosEnterpriseProfile(t *testing.T) profileutil.ProvisioningProfileInfoModel { + now := time.Now() return profileutil.ProvisioningProfileInfoModel{ UUID: "uuid-4", Name: "tvOS enterprise profile", @@ -278,9 +284,9 @@ func tvosEnterpriseProfile(t *testing.T) profileutil.ProvisioningProfileInfoMode BundleID: "io.tvos", ExportType: exportoptions.MethodEnterprise, ProvisionedDevices: nil, - DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, dateRelativeToNow(1, 0, 0))}, - CreationDate: dateRelativeToNow(0, 0, -1), - ExpirationDate: dateRelativeToNow(0, 0, 10), + DeveloperCertificates: []certificateutil.CertificateInfo{distCert(t, now, now.AddDate(1, 0, 0))}, + CreationDate: now.AddDate(0, 0, -1), + ExpirationDate: now.AddDate(0, 0, 10), Entitlements: secondSetOfEntitlements(), ProvisionsAllDevices: true, Type: profileutil.ProfileTypeTvOs, diff --git a/certificateutil/info_model.go b/certificateutil/info_model.go index fba4df1e..0071bbdc 100644 --- a/certificateutil/info_model.go +++ b/certificateutil/info_model.go @@ -10,6 +10,7 @@ import ( "time" "github.com/bitrise-io/go-pkcs12" + "github.com/bitrise-io/go-xcode/v2/timeutil" ) type CertificateInfo struct { @@ -74,16 +75,17 @@ func (info CertificateInfo) String() string { team := fmt.Sprintf("%s (%s)", info.TeamName, info.TeamID) certInfo := fmt.Sprintf("Serial: %s, Name: %s, Team: %s, Expiry: %s", info.Serial, info.CommonName, team, info.EndDate) - err := info.CheckValidity() - if err != nil { - certInfo = certInfo + fmt.Sprintf(", error: %s", err) - } + //if timeProvider != nil { + // if err := info.CheckValidity(*timeProvider); err != nil { + // certInfo = certInfo + fmt.Sprintf(", error: %s", err) + // } + //} return certInfo } -func (info CertificateInfo) CheckValidity() error { - return CheckValidity(info.Certificate) +func (info CertificateInfo) CheckValidity(timeProvider timeutil.TimeProvider) error { + return CheckValidity(info.Certificate, timeProvider) } // EncodeToP12 encodes a CertificateInfo in pkcs12 (.p12) format. diff --git a/certificateutil/security_tool.go b/certificateutil/security_tool.go index 35c06de2..f9fe7dc2 100644 --- a/certificateutil/security_tool.go +++ b/certificateutil/security_tool.go @@ -7,12 +7,19 @@ import ( "regexp" "strings" - "github.com/bitrise-io/go-utils/command" + "github.com/bitrise-io/go-utils/v2/command" ) -// InstalledCodesigningCertificateInfos ... -func InstalledCodesigningCertificateInfos() ([]CertificateInfo, error) { - certificates, err := installedCodesigningCertificates() +type SecurityTool struct { + commandFactory command.Factory +} + +func NewSecurityTool(commandFactory command.Factory) SecurityTool { + return SecurityTool{commandFactory: commandFactory} +} + +func (t SecurityTool) InstalledCodesigningCertificateInfos() ([]CertificateInfo, error) { + certificates, err := t.installedCodesigningCertificates() if err != nil { return nil, err } @@ -27,14 +34,13 @@ func InstalledCodesigningCertificateInfos() ([]CertificateInfo, error) { return infos, nil } -// InstalledInstallerCertificateInfos ... -func InstalledInstallerCertificateInfos() ([]CertificateInfo, error) { - certificates, err := InstalledMacAppStoreCertificates() +func (t SecurityTool) InstalledInstallerCertificateInfos() ([]CertificateInfo, error) { + certificates, err := t.installedMacAppStoreCertificates() if err != nil { return nil, err } - infos := []CertificateInfo{} + var infos []CertificateInfo for _, certificate := range certificates { if certificate != nil { infos = append(infos, NewCertificateInfo(*certificate, nil)) @@ -48,51 +54,48 @@ func InstalledInstallerCertificateInfos() ([]CertificateInfo, error) { return installerCertificates, nil } -// installedCodesigningCertificates ... -func installedCodesigningCertificates() ([]*x509.Certificate, error) { - certificateNames, err := InstalledCodesigningCertificateNames() +func (t SecurityTool) InstalledCodesigningCertificateNames() ([]string, error) { + cmd := t.commandFactory.Create("security", []string{"find-identity", "-v", "-p", "codesigning"}, nil) + out, err := cmd.RunAndReturnTrimmedCombinedOutput() if err != nil { return nil, err } - return getInstalledCertificatesByNameSlice(certificateNames) + return installedCodesigningCertificateNamesFromOutput(out) } -// InstalledMacAppStoreCertificates ... -func InstalledMacAppStoreCertificates() ([]*x509.Certificate, error) { - certificateNames, err := InstalledMacAppStoreCertificateNames() +func (t SecurityTool) InstalledMacAppStoreCertificateNames() ([]string, error) { + cmd := t.commandFactory.Create("security", []string{"find-identity", "-v", "-p", "macappstore"}, nil) + out, err := cmd.RunAndReturnTrimmedCombinedOutput() if err != nil { return nil, err } - return getInstalledCertificatesByNameSlice(certificateNames) + return installedCodesigningCertificateNamesFromOutput(out) } -// InstalledCodesigningCertificateNames ... -func InstalledCodesigningCertificateNames() ([]string, error) { - cmd := command.New("security", "find-identity", "-v", "-p", "codesigning") - out, err := cmd.RunAndReturnTrimmedCombinedOutput() +func (t SecurityTool) installedCodesigningCertificates() ([]*x509.Certificate, error) { + certificateNames, err := t.InstalledCodesigningCertificateNames() if err != nil { - return nil, commandError(cmd.PrintableCommandArgs(), out, err) + return nil, err } - return installedCodesigningCertificateNamesFromOutput(out) + return t.getInstalledCertificatesByNameSlice(certificateNames) } -// InstalledMacAppStoreCertificateNames ... -func InstalledMacAppStoreCertificateNames() ([]string, error) { - cmd := command.New("security", "find-identity", "-v", "-p", "macappstore") - out, err := cmd.RunAndReturnTrimmedCombinedOutput() +func (t SecurityTool) installedMacAppStoreCertificates() ([]*x509.Certificate, error) { + certificateNames, err := t.InstalledMacAppStoreCertificateNames() if err != nil { - return nil, commandError(cmd.PrintableCommandArgs(), out, err) + return nil, err } - return installedCodesigningCertificateNamesFromOutput(out) + return t.getInstalledCertificatesByNameSlice(certificateNames) } -func getInstalledCertificatesByNameSlice(certificateNames []string) ([]*x509.Certificate, error) { - certificates := []*x509.Certificate{} +func (t SecurityTool) getInstalledCertificatesByNameSlice(certificateNames []string) ([]*x509.Certificate, error) { + var certificates []*x509.Certificate + for _, name := range certificateNames { - cmd := command.New("security", "find-certificate", "-c", name, "-p", "-a") + cmd := t.commandFactory.Create("security", []string{"find-certificate", "-c", name, "-p", "-a"}, nil) out, err := cmd.RunAndReturnTrimmedCombinedOutput() if err != nil { - return nil, commandError(cmd.PrintableCommandArgs(), out, err) + return nil, err } normalizedOuts, err := normalizeFindCertificateOut(out) @@ -101,7 +104,7 @@ func getInstalledCertificatesByNameSlice(certificateNames []string) ([]*x509.Cer } for _, normalizedOut := range normalizedOuts { - certificate, err := CeritifcateFromPemContent([]byte(normalizedOut)) + certificate, err := CertificateFromPemContent([]byte(normalizedOut)) if err != nil { return nil, err } @@ -157,7 +160,3 @@ func normalizeFindCertificateOut(out string) ([]string, error) { return certificateContents, nil } - -func commandError(printableCmd string, cmdOut string, cmdErr error) error { - return fmt.Errorf("%s failed, out: %s, err: %w", printableCmd, cmdOut, cmdErr) -} diff --git a/certificateutil/util.go b/certificateutil/util.go index cb5cd819..f7accd98 100644 --- a/certificateutil/util.go +++ b/certificateutil/util.go @@ -10,6 +10,8 @@ import ( "math/big" "sort" "time" + + "github.com/bitrise-io/go-xcode/v2/timeutil" ) // CertificateFromDERContent ... @@ -17,8 +19,8 @@ func CertificateFromDERContent(content []byte) (*x509.Certificate, error) { return x509.ParseCertificate(content) } -// CeritifcateFromPemContent ... -func CeritifcateFromPemContent(content []byte) (*x509.Certificate, error) { +// CertificateFromPemContent ... +func CertificateFromPemContent(content []byte) (*x509.Certificate, error) { block, _ := pem.Decode(content) if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { return nil, fmt.Errorf("failed to parse profile from: %s", string(content)) @@ -27,27 +29,25 @@ func CeritifcateFromPemContent(content []byte) (*x509.Certificate, error) { } // CheckValidity ... -func CheckValidity(certificate x509.Certificate) error { - timeNow := time.Now() +func CheckValidity(certificate x509.Certificate, timeProvider timeutil.TimeProvider) error { + timeNow := timeProvider.CurrentTime() if !timeNow.After(certificate.NotBefore) { - return fmt.Errorf("Certificate is not yet valid - validity starts at: %s", certificate.NotBefore) + return fmt.Errorf("certificate is not yet valid - validity starts at: %s", certificate.NotBefore) } if !timeNow.Before(certificate.NotAfter) { - return fmt.Errorf("Certificate is not valid anymore - validity ended at: %s", certificate.NotAfter) + return fmt.Errorf("certificate is not valid anymore - validity ended at: %s", certificate.NotAfter) } return nil } // FilterCertificateInfoModelsByFilterFunc ... func FilterCertificateInfoModelsByFilterFunc(certificates []CertificateInfo, filterFunc func(certificate CertificateInfo) bool) []CertificateInfo { - filteredCertificates := []CertificateInfo{} - + var filteredCertificates []CertificateInfo for _, certificate := range certificates { if filterFunc(certificate) { filteredCertificates = append(filteredCertificates, certificate) } } - return filteredCertificates } @@ -58,12 +58,12 @@ type ValidCertificateInfo struct { DuplicatedCertificates []CertificateInfo } -// FilterValidCertificateInfos filters out invalid and duplicated common name certificaates -func FilterValidCertificateInfos(certificateInfos []CertificateInfo) ValidCertificateInfo { +// FilterValidCertificateInfos filters out invalid and duplicated common name certificates +func FilterValidCertificateInfos(certificateInfos []CertificateInfo, timeProvider timeutil.TimeProvider) ValidCertificateInfo { var invalidCertificates []CertificateInfo nameToCerts := map[string][]CertificateInfo{} for _, certificateInfo := range certificateInfos { - if certificateInfo.CheckValidity() != nil { + if certificateInfo.CheckValidity(timeProvider) != nil { invalidCertificates = append(invalidCertificates, certificateInfo) continue } @@ -94,7 +94,7 @@ func FilterValidCertificateInfos(certificateInfos []CertificateInfo) ValidCertif } // GenerateTestCertificate creates a certificate (signed by a self-signed CA cert) for test purposes -func GenerateTestCertificate(serial int64, teamID, teamName, commonName string, expiry time.Time) (*x509.Certificate, *rsa.PrivateKey, error) { +func GenerateTestCertificate(serial int64, teamID, teamName, commonName string, notBefore, notAfter time.Time) (*x509.Certificate, *rsa.PrivateKey, error) { CAtemplate := &x509.Certificate{ IsCA: true, BasicConstraintsValid: true, @@ -105,8 +105,8 @@ func GenerateTestCertificate(serial int64, teamID, teamName, commonName string, Organization: []string{"Pear Worldwide Developer Relations"}, CommonName: "Pear Worldwide Developer Relations CA", }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(1, 0, 0), + NotBefore: notBefore, + NotAfter: notBefore.AddDate(1, 0, 0), // see http://golang.org/pkg/crypto/x509/#KeyUsage ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, @@ -137,8 +137,8 @@ func GenerateTestCertificate(serial int64, teamID, teamName, commonName string, OrganizationalUnit: []string{teamID}, CommonName: commonName, }, - NotBefore: time.Now(), - NotAfter: expiry, + NotBefore: notBefore, + NotAfter: notAfter, // see http://golang.org/pkg/crypto/x509/#KeyUsage KeyUsage: x509.KeyUsageDigitalSignature, } @@ -160,3 +160,11 @@ func GenerateTestCertificate(serial int64, teamID, teamName, commonName string, return cert, privatekey, nil } + +func GenerateTestCertificateInfo(serial int64, teamID, teamName, commonName string, notBefore, notAfter time.Time) (CertificateInfo, error) { + cert, privateKey, err := GenerateTestCertificate(serial, teamID, teamName, commonName, notBefore, notAfter) + if err != nil { + return CertificateInfo{}, err + } + return NewCertificateInfo(*cert, privateKey), nil +} diff --git a/certificateutil/util_test.go b/certificateutil/util_test.go index 25e8f99e..44cc48ce 100644 --- a/certificateutil/util_test.go +++ b/certificateutil/util_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/bitrise-io/go-xcode/v2/timeutil" "github.com/stretchr/testify/require" ) @@ -45,25 +46,26 @@ func TestFilterValidCertificateInfos(t *testing.T) { const teamID = "MYTEAMID" const teamName = "BITFALL FEJLESZTO KORLATOLT FELELOSSEGU TARSASAG" const commonName = "Apple Developer: test" - validExpiry := time.Now().AddDate(1, 0, 0) - earlierValidExpiry := time.Now().AddDate(0, 1, 0) - invalidExpiry := time.Now().AddDate(-1, 0, 0) + notBefore := time.Now() + validExpiry := notBefore.AddDate(1, 0, 0) + earlierValidExpiry := notBefore.AddDate(0, 1, 0) + invalidExpiry := notBefore.AddDate(-1, 0, 0) - latestValidCert, privateKey, err := GenerateTestCertificate(serial, teamID, teamName, commonName, validExpiry) + latestValidCert, privateKey, err := GenerateTestCertificate(serial, teamID, teamName, commonName, notBefore, validExpiry) if err != nil { t.Errorf("init: failed to generate certificate, error: %s", err) } latestValidCertInfo := NewCertificateInfo(*latestValidCert, privateKey) t.Logf("Test certificate generated: %s", latestValidCertInfo) - earlierValidCert, privateKey, err := GenerateTestCertificate(serial, teamID, teamName, commonName, earlierValidExpiry) + earlierValidCert, privateKey, err := GenerateTestCertificate(serial, teamID, teamName, commonName, notBefore, earlierValidExpiry) if err != nil { t.Errorf("init: failed to generate certificate, error: %s", err) } earlierValidCertInfo := NewCertificateInfo(*earlierValidCert, privateKey) t.Logf("Test certificate generated: %s", earlierValidCertInfo) - invalidCert, privateKey, err := GenerateTestCertificate(serial, teamID, teamName, commonName, invalidExpiry) + invalidCert, privateKey, err := GenerateTestCertificate(serial, teamID, teamName, commonName, notBefore, invalidExpiry) if err != nil { t.Errorf("init: failed to generate certificate, error: %s", err) } @@ -105,7 +107,7 @@ func TestFilterValidCertificateInfos(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := FilterValidCertificateInfos(tt.certificateInfos); !reflect.DeepEqual(got, tt.want) { + if got := FilterValidCertificateInfos(tt.certificateInfos, timeutil.NewDefaultTimeProvider()); !reflect.DeepEqual(got, tt.want) { t.Errorf("FilterValidCertificateInfos() = %v, want %v", got, tt.want) } }) diff --git a/codesign/codesign.go b/codesign/codesign.go index 361ff69d..993ef658 100644 --- a/codesign/codesign.go +++ b/codesign/codesign.go @@ -11,6 +11,7 @@ import ( "github.com/bitrise-io/go-xcode/v2/autocodesign/projectmanager" "github.com/bitrise-io/go-xcode/v2/certificateutil" "github.com/bitrise-io/go-xcode/v2/devportalservice" + "github.com/bitrise-io/go-xcode/v2/timeutil" "github.com/bitrise-io/go-xcode/v2/xcarchive" ) @@ -62,7 +63,8 @@ type Manager struct { detailsProvider DetailsProvider assetWriter AssetWriter - logger log.Logger + timeProvider timeutil.TimeProvider + logger log.Logger } // NewManagerWithArchive creates a codesign manager, which reads the code signing asset requirements from an XCArchive file. @@ -76,6 +78,7 @@ func NewManagerWithArchive( assetInstaller autocodesign.AssetWriter, localCodeSignAssetManager autocodesign.LocalCodeSignAssetManager, archive xcarchive.IosArchive, + timeProvider timeutil.TimeProvider, logger log.Logger, ) Manager { return Manager{ @@ -88,6 +91,7 @@ func NewManagerWithArchive( assetInstaller: assetInstaller, localCodeSignAssetManager: localCodeSignAssetManager, detailsProvider: archive, + timeProvider: timeProvider, logger: logger, } } @@ -324,7 +328,7 @@ func (m *Manager) installCertificates(certificates []certificateutil.Certificate } func (m *Manager) validateCertificatesForXcodeManagedSigning(certificates []certificateutil.CertificateInfo) error { - typeToLocalCerts, err := autocodesign.GetValidLocalCertificates(certificates) + typeToLocalCerts, err := autocodesign.GetValidLocalCertificates(certificates, m.timeProvider) if err != nil { return err } @@ -379,7 +383,7 @@ func (m *Manager) prepareCodeSigningWithBitrise(credentials devportalservice.Cre return err } - typeToLocalCerts, err := autocodesign.GetValidLocalCertificates(certs) + typeToLocalCerts, err := autocodesign.GetValidLocalCertificates(certs, m.timeProvider) if err != nil { return err } diff --git a/codesign/codesign_test.go b/codesign/codesign_test.go index ba2b0363..8d8c522e 100644 --- a/codesign/codesign_test.go +++ b/codesign/codesign_test.go @@ -190,9 +190,10 @@ func generateCert(t *testing.T, commonName string) certificateutil.CertificateIn teamID = "MYTEAMID" teamName = "BITFALL FEJLESZTO KORLATOLT FELELOSSEGU TARSASAG" ) - expiry := time.Now().AddDate(1, 0, 0) + notBefore := time.Now() + expiry := notBefore.AddDate(1, 0, 0) - cert, privateKey, err := certificateutil.GenerateTestCertificate(int64(1), teamID, teamName, commonName, expiry) + cert, privateKey, err := certificateutil.GenerateTestCertificate(int64(1), teamID, teamName, commonName, notBefore, expiry) if err != nil { t.Fatalf("init: failed to generate certificate: %s", err) } diff --git a/exportoptionsgenerator/certificates.go b/exportoptionsgenerator/certificates.go index 31ae6eb1..9663f98d 100644 --- a/exportoptionsgenerator/certificates.go +++ b/exportoptionsgenerator/certificates.go @@ -1,6 +1,10 @@ package exportoptionsgenerator -import "github.com/bitrise-io/go-xcode/v2/certificateutil" +import ( + "github.com/bitrise-io/go-utils/v2/command" + "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/timeutil" +) // CodesignIdentityProvider can list certificate infos. type CodesignIdentityProvider interface { @@ -8,14 +12,25 @@ type CodesignIdentityProvider interface { } // LocalCodesignIdentityProvider ... -type LocalCodesignIdentityProvider struct{} +type LocalCodesignIdentityProvider struct { + commandFactory command.Factory + timeProvider timeutil.TimeProvider +} + +func NewLocalCodesignIdentityProvider(commandFactory command.Factory, timeProvider timeutil.TimeProvider) LocalCodesignIdentityProvider { + return LocalCodesignIdentityProvider{ + commandFactory: commandFactory, + timeProvider: timeProvider, + } +} // ListCodesignIdentities ... func (p LocalCodesignIdentityProvider) ListCodesignIdentities() ([]certificateutil.CertificateInfo, error) { - certs, err := certificateutil.InstalledCodesigningCertificateInfos() + securityTool := certificateutil.NewSecurityTool(p.commandFactory) + certs, err := securityTool.InstalledCodesigningCertificateInfos() if err != nil { return nil, err } - certInfo := certificateutil.FilterValidCertificateInfos(certs) + certInfo := certificateutil.FilterValidCertificateInfos(certs, p.timeProvider) return append(certInfo.ValidCertificates, certInfo.DuplicatedCertificates...), nil } diff --git a/timeutil/time_provider.go b/timeutil/time_provider.go new file mode 100644 index 00000000..292cc87e --- /dev/null +++ b/timeutil/time_provider.go @@ -0,0 +1,17 @@ +package timeutil + +import "time" + +type TimeProvider interface { + CurrentTime() time.Time +} + +type DefaultTimeProvider struct{} + +func NewDefaultTimeProvider() DefaultTimeProvider { + return DefaultTimeProvider{} +} + +func (d DefaultTimeProvider) CurrentTime() time.Time { + return time.Now() +}