diff --git a/_integration_tests/zip/ipa_reader_test.go b/_integration_tests/zip/ipa_reader_test.go index 54a20b1d..19f74356 100644 --- a/_integration_tests/zip/ipa_reader_test.go +++ b/_integration_tests/zip/ipa_reader_test.go @@ -6,11 +6,11 @@ import ( "testing" "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/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 0ed698ee..00684404 100644 --- a/artifacts/ipa_reader.go +++ b/artifacts/ipa_reader.go @@ -3,8 +3,8 @@ package artifacts import ( "fmt" - "github.com/bitrise-io/go-xcode/plistutil" - "github.com/bitrise-io/go-xcode/profileutil" + "github.com/bitrise-io/go-xcode/v2/plistutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" ) // IPAReader ... @@ -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/autocodesign/autocodesign.go b/autocodesign/autocodesign.go index 12546aa4..43fe6b38 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/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/v2/profileutil" "github.com/bitrise-io/go-xcode/xcodeproject/serialized" ) @@ -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 76eda4ee..56dae8a0 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" @@ -62,22 +62,16 @@ func newMockProfile(m profileArgs) Profile { return profile } -func newCertificate(t *testing.T, teamID, teamName, commonName string, expiry time.Time) certificateutil.CertificateInfoModel { - 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{ @@ -511,7 +514,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 +531,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 fe5a988a..507a39f3 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 @@ -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 627c8c74..b5e4007e 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" ) @@ -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,16 +91,17 @@ 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" 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 b3d04028..15106a30 100644 --- a/autocodesign/certificates.go +++ b/autocodesign/certificates.go @@ -5,8 +5,9 @@ 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" + "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.CertificateInfoModel) (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.CertificateInfoMod 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)) @@ -113,7 +114,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 +151,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, timeProvider timeutil.TimeProvider) []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 +179,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 5e0af9e0..ae80175a 100644 --- a/autocodesign/certificates_test.go +++ b/autocodesign/certificates_test.go @@ -8,8 +8,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" + "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,32 +347,33 @@ 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) 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 +388,7 @@ func TestGetValidLocalCertificates(t *testing.T) { }, { name: "dev + dist cert", - certificates: []certificateutil.CertificateInfoModel{ + certificates: []certificateutil.CertificateInfo{ distributionCert, devCert, }, @@ -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/codesignasset/writer.go b/autocodesign/codesignasset/writer.go index 906c5b5e..5a746282 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 ( @@ -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/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..6f36cc3b 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 ... @@ -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.go b/autocodesign/keychain/keychain.go index 63cd8ce0..09188625 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" ) @@ -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/keychain/keychain_test.go b/autocodesign/keychain/keychain_test.go index 1928e552..7cbef365 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) { @@ -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 c017955e..3a7612a1 100644 --- a/autocodesign/localcodesignasset/localcodesignasset_test.go +++ b/autocodesign/localcodesignasset/localcodesignasset_test.go @@ -5,13 +5,14 @@ import ( "time" devportaltime "github.com/bitrise-io/go-xcode/v2/autocodesign/devportalclient/time" + "github.com/stretchr/testify/require" - "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/bitrise-io/go-xcode/v2/exportoptions" + "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -177,7 +178,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 +189,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 { @@ -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.CertificateInfoModel{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.CertificateInfoModel{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.CertificateInfoModel{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.CertificateInfoModel{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.CertificateInfoModel { - 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.CertificateInfoModel { - 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 { - 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/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 84765fb5..94199bdb 100644 --- a/autocodesign/localcodesignasset/profilelookup_test.go +++ b/autocodesign/localcodesignasset/profilelookup_test.go @@ -2,11 +2,12 @@ package localcodesignasset import ( "testing" + "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/certificateutil" + "github.com/bitrise-io/go-xcode/v2/exportoptions" + "github.com/bitrise-io/go-xcode/v2/profileutil" "github.com/stretchr/testify/assert" ) @@ -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.CertificateInfoModel{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.CertificateInfoModel{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.CertificateInfoModel{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.CertificateInfoModel{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.CertificateInfoModel{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/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/mock_AssetWriter.go b/autocodesign/mock_AssetWriter.go index d95af49f..f77e5da3 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" ) @@ -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 3f67bf73..ef747bec 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" ) @@ -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/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/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/autocodesign/utils_test.go b/autocodesign/utils_test.go index 55a54949..463976da 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" ) @@ -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 new file mode 100644 index 00000000..0071bbdc --- /dev/null +++ b/certificateutil/info_model.go @@ -0,0 +1,94 @@ +package certificateutil + +import ( + "crypto/rand" + "crypto/sha1" + "crypto/x509" + "errors" + "fmt" + "strings" + "time" + + "github.com/bitrise-io/go-pkcs12" + "github.com/bitrise-io/go-xcode/v2/timeutil" +) + +type CertificateInfo struct { + CommonName string + TeamName string + TeamID string + EndDate time.Time + StartDate time.Time + + Serial string + SHA1Fingerprint string + + Certificate x509.Certificate + PrivateKey interface{} +} + +func NewCertificateInfo(certificate x509.Certificate, privateKey interface{}) CertificateInfo { + fingerprint := sha1.Sum(certificate.Raw) + fingerprintStr := fmt.Sprintf("%x", fingerprint) + + return CertificateInfo{ + 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 CertificateInfo +// Used to parse p12 file containing multiple codesign identities (exported from macOS Keychain) +func CertificatesFromPKCS12Content(content []byte, password string) ([]CertificateInfo, 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 := []CertificateInfo{} + for i, certificate := range certificates { + if certificate != nil { + infos = append(infos, NewCertificateInfo(*certificate, privateKeys[i])) + } + } + + return infos, nil +} + +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) + + //if timeProvider != nil { + // if err := info.CheckValidity(*timeProvider); err != nil { + // certInfo = certInfo + fmt.Sprintf(", error: %s", err) + // } + //} + + return certInfo +} + +func (info CertificateInfo) CheckValidity(timeProvider timeutil.TimeProvider) error { + return CheckValidity(info.Certificate, timeProvider) +} + +// 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 new file mode 100644 index 00000000..f9fe7dc2 --- /dev/null +++ b/certificateutil/security_tool.go @@ -0,0 +1,162 @@ +package certificateutil + +import ( + "bufio" + "crypto/x509" + "fmt" + "regexp" + "strings" + + "github.com/bitrise-io/go-utils/v2/command" +) + +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 + } + + infos := []CertificateInfo{} + for _, certificate := range certificates { + if certificate != nil { + infos = append(infos, NewCertificateInfo(*certificate, nil)) + } + } + + return infos, nil +} + +func (t SecurityTool) InstalledInstallerCertificateInfos() ([]CertificateInfo, error) { + certificates, err := t.installedMacAppStoreCertificates() + if err != nil { + return nil, err + } + + var infos []CertificateInfo + for _, certificate := range certificates { + if certificate != nil { + infos = append(infos, NewCertificateInfo(*certificate, nil)) + } + } + + installerCertificates := FilterCertificateInfoModelsByFilterFunc(infos, func(cert CertificateInfo) bool { + return strings.Contains(cert.CommonName, "Installer") + }) + + return installerCertificates, nil +} + +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 installedCodesigningCertificateNamesFromOutput(out) +} + +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 installedCodesigningCertificateNamesFromOutput(out) +} + +func (t SecurityTool) installedCodesigningCertificates() ([]*x509.Certificate, error) { + certificateNames, err := t.InstalledCodesigningCertificateNames() + if err != nil { + return nil, err + } + return t.getInstalledCertificatesByNameSlice(certificateNames) +} + +func (t SecurityTool) installedMacAppStoreCertificates() ([]*x509.Certificate, error) { + certificateNames, err := t.InstalledMacAppStoreCertificateNames() + if err != nil { + return nil, err + } + return t.getInstalledCertificatesByNameSlice(certificateNames) +} + +func (t SecurityTool) getInstalledCertificatesByNameSlice(certificateNames []string) ([]*x509.Certificate, error) { + var certificates []*x509.Certificate + + for _, name := range certificateNames { + cmd := t.commandFactory.Create("security", []string{"find-certificate", "-c", name, "-p", "-a"}, nil) + out, err := cmd.RunAndReturnTrimmedCombinedOutput() + if err != nil { + return nil, err + } + + normalizedOuts, err := normalizeFindCertificateOut(out) + if err != nil { + return nil, err + } + + for _, normalizedOut := range normalizedOuts { + certificate, err := CertificateFromPemContent([]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 +} diff --git a/certificateutil/util.go b/certificateutil/util.go new file mode 100644 index 00000000..f7accd98 --- /dev/null +++ b/certificateutil/util.go @@ -0,0 +1,170 @@ +package certificateutil + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "sort" + "time" + + "github.com/bitrise-io/go-xcode/v2/timeutil" +) + +// CertificateFromDERContent ... +func CertificateFromDERContent(content []byte) (*x509.Certificate, error) { + return x509.ParseCertificate(content) +} + +// 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)) + } + return CertificateFromDERContent(block.Bytes) +} + +// CheckValidity ... +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) + } + 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 []CertificateInfo, filterFunc func(certificate CertificateInfo) bool) []CertificateInfo { + var filteredCertificates []CertificateInfo + 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 []CertificateInfo +} + +// 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(timeProvider) != nil { + invalidCertificates = append(invalidCertificates, certificateInfo) + continue + } + + nameToCerts[certificateInfo.CommonName] = append(nameToCerts[certificateInfo.CommonName], certificateInfo) + } + + var validCertificates, duplicatedCertificates []CertificateInfo + 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, notBefore, notAfter 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: 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, + } + + 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: notBefore, + NotAfter: notAfter, + // 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 +} + +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 new file mode 100644 index 00000000..44cc48ce --- /dev/null +++ b/certificateutil/util_test.go @@ -0,0 +1,115 @@ +package certificateutil + +import ( + "reflect" + "strings" + "testing" + "time" + + "github.com/bitrise-io/go-xcode/v2/timeutil" + "github.com/stretchr/testify/require" +) + +func TestFilterCertificateInfoModelsByFilterFunc(t *testing.T) { + 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 := []CertificateInfo{ + CertificateInfo{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 := []CertificateInfo{ + CertificateInfo{TeamID: "test-team-id2", 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 := []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 CertificateInfo) 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" + 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, 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, 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, notBefore, 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 []CertificateInfo + want ValidCertificateInfo + }{ + { + name: "one valid cert", + certificateInfos: []CertificateInfo{latestValidCertInfo}, + want: ValidCertificateInfo{ + ValidCertificates: []CertificateInfo{latestValidCertInfo}, + InvalidCertificates: nil, + DuplicatedCertificates: nil, + }, + }, + { + name: "one valid, one invalid cert with same name", + certificateInfos: []CertificateInfo{latestValidCertInfo, invalidCertInfo}, + want: ValidCertificateInfo{ + ValidCertificates: []CertificateInfo{latestValidCertInfo}, + InvalidCertificates: []CertificateInfo{invalidCertInfo}, + DuplicatedCertificates: nil, + }, + }, + { + name: "2 valid, duplicated certs", + certificateInfos: []CertificateInfo{latestValidCertInfo, earlierValidCertInfo, invalidCertInfo}, + want: ValidCertificateInfo{ + ValidCertificates: []CertificateInfo{latestValidCertInfo}, + InvalidCertificates: []CertificateInfo{invalidCertInfo}, + DuplicatedCertificates: []CertificateInfo{earlierValidCertInfo}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + 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 feaeba58..993ef658 100644 --- a/codesign/codesign.go +++ b/codesign/codesign.go @@ -5,12 +5,13 @@ 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/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, } } @@ -292,7 +296,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 +316,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,8 +327,8 @@ func (m *Manager) installCertificates(certificates []certificateutil.Certificate return nil } -func (m *Manager) validateCertificatesForXcodeManagedSigning(certificates []certificateutil.CertificateInfoModel) error { - typeToLocalCerts, err := autocodesign.GetValidLocalCertificates(certificates) +func (m *Manager) validateCertificatesForXcodeManagedSigning(certificates []certificateutil.CertificateInfo) error { + 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 } @@ -438,7 +442,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 fc585c05..8d8c522e 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" @@ -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,14 +185,15 @@ 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" ) - 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/export/codesing_group.go b/export/codesing_group.go new file mode 100644 index 00000000..669eee51 --- /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.CertificateInfo + InstallerCertificate() *certificateutil.CertificateInfo + BundleIDProfileMap() map[string]profileutil.ProvisioningProfileInfoModel +} diff --git a/export/export.go b/export/export.go new file mode 100644 index 00000000..c8c4042f --- /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.CertificateInfo + 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.CertificateInfo, certificate certificateutil.CertificateInfo) bool { + for _, cert := range installedCertificates { + if cert.Serial == certificate.Serial { + return true + } + } + return false +} + +// CreateSelectableCodeSignGroups ... +func CreateSelectableCodeSignGroups(certificates []certificateutil.CertificateInfo, profiles []profileutil.ProvisioningProfileInfoModel, bundleIDs []string) []SelectableCodeSignGroup { + groups := []SelectableCodeSignGroup{} + + serialProfilesMap := map[string][]profileutil.ProvisioningProfileInfoModel{} + serialCertificateMap := map[string]certificateutil.CertificateInfo{} + 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..7410acde --- /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/v2/exportoptions" + "github.com/bitrise-io/go-xcode/v2/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.MapData) 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..69ab71e8 --- /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.CertificateInfo + bundleIDProfileMap map[string]profileutil.ProvisioningProfileInfoModel +} + +// Certificate ... +func (signGroup *IosCodeSignGroup) Certificate() certificateutil.CertificateInfo { + return signGroup.certificate +} + +// InstallerCertificate ... +func (signGroup *IosCodeSignGroup) InstallerCertificate() *certificateutil.CertificateInfo { + return nil +} + +// BundleIDProfileMap ... +func (signGroup *IosCodeSignGroup) BundleIDProfileMap() map[string]profileutil.ProvisioningProfileInfoModel { + return signGroup.bundleIDProfileMap +} + +// NewIOSGroup ... +func NewIOSGroup(certificate certificateutil.CertificateInfo, 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..1f935bd1 --- /dev/null +++ b/export/mac.go @@ -0,0 +1,73 @@ +package export + +import ( + "github.com/bitrise-io/go-xcode/v2/certificateutil" + "github.com/bitrise-io/go-xcode/v2/exportoptions" + "github.com/bitrise-io/go-xcode/v2/profileutil" +) + +// MacCodeSignGroup ... +type MacCodeSignGroup struct { + certificate certificateutil.CertificateInfo + installerCertificate *certificateutil.CertificateInfo + bundleIDProfileMap map[string]profileutil.ProvisioningProfileInfoModel +} + +// Certificate ... +func (signGroup *MacCodeSignGroup) Certificate() certificateutil.CertificateInfo { + return signGroup.certificate +} + +// InstallerCertificate ... +func (signGroup *MacCodeSignGroup) InstallerCertificate() *certificateutil.CertificateInfo { + return signGroup.installerCertificate +} + +// BundleIDProfileMap ... +func (signGroup *MacCodeSignGroup) BundleIDProfileMap() map[string]profileutil.ProvisioningProfileInfoModel { + return signGroup.bundleIDProfileMap +} + +// NewMacGroup ... +func NewMacGroup(certificate certificateutil.CertificateInfo, installerCertificate *certificateutil.CertificateInfo, bundleIDProfileMap map[string]profileutil.ProvisioningProfileInfoModel) *MacCodeSignGroup { + return &MacCodeSignGroup{ + certificate: certificate, + installerCertificate: installerCertificate, + bundleIDProfileMap: bundleIDProfileMap, + } +} + +// CreateMacCodeSignGroup ... +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.CertificateInfo{} + + 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/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/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/exportoptionsgenerator/certificates.go b/exportoptionsgenerator/certificates.go index d916ce54..9663f98d 100644 --- a/exportoptionsgenerator/certificates.go +++ b/exportoptionsgenerator/certificates.go @@ -1,21 +1,36 @@ package exportoptionsgenerator -import "github.com/bitrise-io/go-xcode/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 { - ListCodesignIdentities() ([]certificateutil.CertificateInfoModel, error) + ListCodesignIdentities() ([]certificateutil.CertificateInfo, error) } // 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.CertificateInfoModel, error) { - certs, err := certificateutil.InstalledCodesigningCertificateInfos() +func (p LocalCodesignIdentityProvider) ListCodesignIdentities() ([]certificateutil.CertificateInfo, error) { + 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/exportoptionsgenerator/exportoptionsgenerator.go b/exportoptionsgenerator/exportoptionsgenerator.go index feb6bf4a..ebb412ec 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/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 20ade1af..92414e0d 100644 --- a/exportoptionsgenerator/exportoptionsgenerator_test.go +++ b/exportoptionsgenerator/exportoptionsgenerator_test.go @@ -5,11 +5,11 @@ 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/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"}}, }, }, @@ -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{ @@ -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, }, @@ -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,13 +534,13 @@ func TestExportOptionsGenerator_GenerateApplicationExportOptions_WhenNoProfileFo g := New(xcodeVersionReader, logger) g.certificateProvider = MockCodesignIdentityProvider{ - []certificateutil.CertificateInfoModel{certificate}, + []certificateutil.CertificateInfo{certificate}, } g.profileProvider = MockProvisioningProfileProvider{} archiveInfo := ArchiveInfo{ AppBundleID: bundleID, - EntitlementsByBundleID: map[string]plistutil.PlistData{ + EntitlementsByBundleID: map[string]plistutil.MapData{ bundleID: cloudKitEntitlement, bundleIDClip: nil, }, @@ -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/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/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/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 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/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 new file mode 100644 index 00000000..fb076f4b --- /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/v2/plistutil" +) + +// MatchTargetAndProfileEntitlements ... +func MatchTargetAndProfileEntitlements(targetEntitlements plistutil.MapData, profileEntitlements plistutil.MapData, 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..819d966e --- /dev/null +++ b/profileutil/info_model.go @@ -0,0 +1,255 @@ +package profileutil + +import ( + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/bitrise-io/go-plist" + + "github.com/bitrise-io/go-utils/log" + "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" +) + +// ProvisioningProfileInfoModel ... +type ProvisioningProfileInfoModel struct { + UUID string + Name string + TeamName string + TeamID string + BundleID string + ExportType exportoptions.Method + ProvisionedDevices []string + DeveloperCertificates []certificateutil.CertificateInfo + CreationDate time.Time + ExpirationDate time.Time + Entitlements plistutil.MapData + ProvisionsAllDevices bool + Type ProfileType +} + +func collectCapabilitesPrintableInfo(entitlements plistutil.MapData) 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.CertificateInfo) 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.CertificateInfo) 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.MapData + 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..cb3b22aa --- /dev/null +++ b/profileutil/plist_data.go @@ -0,0 +1,171 @@ +package profileutil + +import ( + "strings" + "time" + + "github.com/bitrise-io/go-plist" + "github.com/bitrise-io/go-xcode/v2/exportoptions" + "github.com/bitrise-io/go-xcode/v2/plistutil" +) + +// PlistData ... +type PlistData plistutil.MapData + +// NewPlistDataFromFile ... +func NewPlistDataFromFile(provisioningProfilePth string) (PlistData, error) { + provisioningProfilePKCS7, err := ProvisioningProfileFromFile(provisioningProfilePth) + if err != nil { + return PlistData{}, err + } + + var plistData plistutil.MapData + if _, err := plist.Unmarshal(provisioningProfilePKCS7.Content, &plistData); err != nil { + return PlistData{}, err + } + + return PlistData(plistData), nil +} + +// GetUUID ... +func (profile PlistData) GetUUID() string { + data := plistutil.MapData(profile) + uuid, _ := data.GetString("UUID") + return uuid +} + +// GetName ... +func (profile PlistData) GetName() string { + data := plistutil.MapData(profile) + uuid, _ := data.GetString("Name") + return uuid +} + +// GetApplicationIdentifier ... +func (profile PlistData) GetApplicationIdentifier() string { + data := plistutil.MapData(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.MapData(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.MapData(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.MapData { + data := plistutil.MapData(profile) + entitlements, _ := data.GetMapStringInterface("Entitlements") + return entitlements +} + +// GetTeamID ... +func (profile PlistData) GetTeamID() string { + data := plistutil.MapData(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.MapData(profile) + expiry, _ := data.GetTime("ExpirationDate") + return expiry +} + +// GetProvisionedDevices ... +func (profile PlistData) GetProvisionedDevices() []string { + data := plistutil.MapData(profile) + devices, _ := data.GetStringArray("ProvisionedDevices") + return devices +} + +// GetDeveloperCertificates ... +func (profile PlistData) GetDeveloperCertificates() [][]byte { + data := plistutil.MapData(profile) + developerCertificates, _ := data.GetByteArrayArray("DeveloperCertificates") + return developerCertificates +} + +// GetTeamName ... +func (profile PlistData) GetTeamName() string { + data := plistutil.MapData(profile) + teamName, _ := data.GetString("TeamName") + return teamName +} + +// GetCreationDate ... +func (profile PlistData) GetCreationDate() time.Time { + data := plistutil.MapData(profile) + creationDate, _ := data.GetTime("CreationDate") + return creationDate +} + +// GetProvisionsAllDevices ... +func (profile PlistData) GetProvisionsAllDevices() bool { + data := plistutil.MapData(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..e1971738 --- /dev/null +++ b/profileutil/plist_data_test.go @@ -0,0 +1,375 @@ +package profileutil + +import ( + "testing" + + "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.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()) + 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.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()) + 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.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()) + 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.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()) + 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.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()) + 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/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() +} diff --git a/xcarchive/ios.go b/xcarchive/ios.go index b854c3c3..0682cf1d 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/v2/autocodesign" - "github.com/bitrise-io/go-xcode/xcarchive" + "github.com/bitrise-io/go-xcode/v2/plistutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" ) +// IosBaseApplication ... +type IosBaseApplication struct { + Path string + InfoPlist plistutil.MapData + Entitlements plistutil.MapData + 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.MapData + { + 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.NewMapDataFromPlistFile(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.MapData + Application IosApplication } // NewIosArchive ... func NewIosArchive(path string) (IosArchive, error) { - archive, err := xcarchive.NewIosArchive(path) + var infoPlist plistutil.MapData + { + 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.NewMapDataFromPlistFile(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.MapData) (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.MapData { + bundleIDEntitlementsMap := map[string]plistutil.MapData{} + + 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 cbd28bcd..924fab24 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-xcode/profileutil" + "github.com/bitrise-io/go-utils/command" + "github.com/bitrise-io/go-utils/pathutil" "github.com/bitrise-io/go-xcode/v2/autocodesign" - v1xcarchive "github.com/bitrise-io/go-xcode/xcarchive" + "github.com/bitrise-io/go-xcode/v2/plistutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" + "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.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.MapData + } + 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..bf153c89 --- /dev/null +++ b/xcarchive/macos.go @@ -0,0 +1,231 @@ +package xcarchive + +import ( + "fmt" + "path/filepath" + + "github.com/bitrise-io/go-xcode/v2/plistutil" + "github.com/bitrise-io/go-xcode/v2/profileutil" + + "github.com/bitrise-io/go-utils/pathutil" +) + +type macosBaseApplication struct { + Path string + InfoPlist plistutil.MapData + Entitlements plistutil.MapData + 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.MapData + { + 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.NewMapDataFromPlistFile(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.MapData + Application MacosApplication +} + +// NewMacosArchive ... +func NewMacosArchive(path string) (MacosArchive, error) { + var infoPlist plistutil.MapData + { + 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.NewMapDataFromPlistFile(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.MapData { + bundleIDEntitlementsMap := map[string]plistutil.MapData{} + + bundleID := archive.Application.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = archive.Application.Entitlements + + for _, plugin := range archive.Application.Extensions { + bundleID := plugin.BundleIdentifier() + bundleIDEntitlementsMap[bundleID] = plugin.Entitlements + } + + return bundleIDEntitlementsMap +} + +// BundleIDProfileInfoMap ... +func (archive MacosArchive) BundleIDProfileInfoMap() map[string]profileutil.ProvisioningProfileInfoModel { + bundleIDProfileMap := map[string]profileutil.ProvisioningProfileInfoModel{} + + if archive.Application.ProvisioningProfile != nil { + bundleID := archive.Application.BundleIdentifier() + bundleIDProfileMap[bundleID] = *archive.Application.ProvisioningProfile + } + + for _, plugin := range archive.Application.Extensions { + if plugin.ProvisioningProfile != nil { + bundleID := plugin.BundleIdentifier() + bundleIDProfileMap[bundleID] = *plugin.ProvisioningProfile + } + } + + return bundleIDProfileMap +} + +// FindDSYMs ... +func (archive MacosArchive) FindDSYMs() ([]string, []string, error) { + return findDSYMs(archive.Path) +} diff --git a/xcarchive/macos_test.go b/xcarchive/macos_test.go new file mode 100644 index 00000000..335af258 --- /dev/null +++ b/xcarchive/macos_test.go @@ -0,0 +1,78 @@ +package xcarchive + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewMacosArchive(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + require.Equal(t, 5, len(archive.InfoPlist)) + + app := archive.Application + require.Equal(t, 21, len(app.InfoPlist)) + require.Equal(t, 2, len(app.Entitlements)) + require.Nil(t, app.ProvisioningProfile) + + require.Equal(t, 1, len(app.Extensions)) + extension := app.Extensions[0] + require.Equal(t, 22, len(extension.InfoPlist)) + require.Equal(t, 2, len(extension.Entitlements)) + require.Nil(t, extension.ProvisioningProfile) +} + +func TestMacosIsXcodeManaged(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + require.Equal(t, false, archive.IsXcodeManaged()) +} + +func TestMacosSigningIdentity(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + require.Equal(t, "Mac Developer: Gödrei Krisztian (T3694PR6UJ)", archive.SigningIdentity()) +} + +func TestMacosBundleIDEntitlementsMap(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + bundleIDEntitlementsMap := archive.BundleIDEntitlementsMap() + require.Equal(t, 2, len(bundleIDEntitlementsMap)) + + bundleIDs := []string{"io.bitrise.archive.Test", "io.bitrise.archive.Test.ActionExtension"} + for _, bundleID := range bundleIDs { + _, ok := bundleIDEntitlementsMap[bundleID] + require.True(t, ok, fmt.Sprintf("%v", bundleIDEntitlementsMap)) + } +} + +func TestMacosBundleIDProfileInfoMap(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + bundleIDProfileInfoMap := archive.BundleIDProfileInfoMap() + require.Equal(t, 0, len(bundleIDProfileInfoMap)) +} + +func TestMacosFindDSYMs(t *testing.T) { + macosArchivePth := filepath.Join(sampleRepoPath(t), "archives/macos.xcarchive") + archive, err := NewMacosArchive(macosArchivePth) + require.NoError(t, err) + + appDsym, otherDsyms, err := archive.FindDSYMs() + require.NoError(t, err) + require.Equal(t, 1, len(appDsym)) + require.Equal(t, 1, len(otherDsyms)) +} diff --git a/xcarchive/utils.go b/xcarchive/utils.go new file mode 100644 index 00000000..8933e6b9 --- /dev/null +++ b/xcarchive/utils.go @@ -0,0 +1,68 @@ +package xcarchive + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/bitrise-io/go-utils/command" + "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-xcode/v2/plistutil" +) + +func executableNameFromInfoPlist(infoPlist plistutil.MapData) string { + if name, ok := infoPlist.GetString("CFBundleExecutable"); ok { + return name + } + return "" +} + +func getEntitlements(basePath, executableRelativePath string) (plistutil.MapData, error) { + entitlements, err := entitlementsFromExecutable(basePath, executableRelativePath) + if err != nil { + return plistutil.MapData{}, err + } + + if entitlements != nil { + return *entitlements, nil + } + + return plistutil.MapData{}, nil +} + +func entitlementsFromExecutable(basePath, executableRelativePath string) (*plistutil.MapData, 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.NewMapDataFromPlistContent(entitlementsString) + if err != nil { + return nil, err + } + + return &plist, nil +} + +func findDSYMs(archivePath string) ([]string, []string, error) { + dsymsDirPth := filepath.Join(archivePath, "dSYMs") + dsyms, err := pathutil.ListEntries(dsymsDirPth, pathutil.ExtensionFilter(".dsym", true)) + if err != nil { + return []string{}, []string{}, err + } + + appDSYMs := []string{} + frameworkDSYMs := []string{} + for _, dsym := range dsyms { + if strings.HasSuffix(dsym, ".app.dSYM") { + appDSYMs = append(appDSYMs, dsym) + } else { + frameworkDSYMs = append(frameworkDSYMs, dsym) + } + } + + return appDSYMs, frameworkDSYMs, nil +} diff --git a/xcarchive/utils_test.go b/xcarchive/utils_test.go new file mode 100644 index 00000000..e8c13020 --- /dev/null +++ b/xcarchive/utils_test.go @@ -0,0 +1,196 @@ +package xcarchive + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-io/go-xcode/v2/plistutil" + "github.com/stretchr/testify/assert" +) + +const ( + tempDirName = "__artifacts__" + DSYMSDirName = "dSYMs" +) + +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.NewMapDataFromPlistFile(infoPlistPath) + if err != nil { + return "" + } + + return filepath.Join(executableFolderRelativePath, executableNameFromInfoPlist(plist)) +} + +func iosEntitlements() plistutil.MapData { + 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.MapData { + return map[string]interface{}{ + "com.apple.security.app-sandbox": true, + "com.apple.security.files.user-selected.read-only": true, + } +} + +func Test_GivenArchiveWithMultipleAppAndFrameworkDSYMs_WhenFindDSYMsCalled_ThenExpectAllDSYMsToBeReturned(t *testing.T) { + testCases := []struct { + name string + numberOfAppDSYMs int + numberOfFrameworkDSYMs int + }{ + { + name: "1. Given archive with multiple app and framework dSYMs when FindDSYMs called then expect all dSYMs to be returned", + numberOfAppDSYMs: 2, + numberOfFrameworkDSYMs: 2, + }, + { + name: "2. Given archive with singe app and framework dSYMs when FindDSYMs called then expect both dSYMs to be returned", + numberOfAppDSYMs: 1, + numberOfFrameworkDSYMs: 1, + }, + { + name: "3. Given archive with multiple app dSYMs when FindDSYMs called then expect all app dSYMs to be returned", + numberOfAppDSYMs: 2, + numberOfFrameworkDSYMs: 0, + }, + { + name: "4. Given archive with multiple framework dSYMs when FindDSYMs called then expect all framework dSYMs to be returned", + numberOfAppDSYMs: 0, + numberOfFrameworkDSYMs: 2, + }, + { + name: "5. Given archive with single app dSYM when FindDSYMs called then expect the app dSYM to be returned", + numberOfAppDSYMs: 1, + numberOfFrameworkDSYMs: 0, + }, + { + name: "6. Given archive with single framework dSYM when FindDSYMs called then expect the framework dSYM to be returned", + numberOfAppDSYMs: 0, + numberOfFrameworkDSYMs: 1, + }, + { + name: "7. Given archive without any dSYM when FindDSYMs called then expect no dSYM to be returned", + numberOfAppDSYMs: 0, + numberOfFrameworkDSYMs: 0, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + archivePath, err := createArchiveWithAppAndFrameworkDSYMs( + "archives/ios.dsyms.xcarchive", + testCase.numberOfAppDSYMs, + testCase.numberOfFrameworkDSYMs, + ) + assert.NoError(t, err) + + appDSYMs, frameworkDSYMs, err := findDSYMs(archivePath) + assert.NoError(t, err) + assert.Equal(t, testCase.numberOfAppDSYMs, len(appDSYMs)) + assert.Equal(t, testCase.numberOfFrameworkDSYMs, len(frameworkDSYMs)) + }) + } +} + +func createArchiveWithAppAndFrameworkDSYMs(archivePath string, numberOfAppDSYMs, numberOfFrameworkDSYMs int) (string, error) { + archivePath, err := createArchive(archivePath) + if err != nil { + return "", err + } + + err = createAppDSYMs(archivePath, numberOfAppDSYMs) + if err != nil { + return "", err + } + + err = createFrameworkDSYMs(archivePath, numberOfFrameworkDSYMs) + if err != nil { + return "", err + } + + return archivePath, nil +} + +func createAppDSYMs(archivePath string, numberOfDSYMs int) error { + return createDSYMs(archivePath, "app", numberOfDSYMs) +} + +func createFrameworkDSYMs(archivePath string, numberOfDSYMs int) error { + return createDSYMs(archivePath, "framework", numberOfDSYMs) +} + +func createDSYMs(archivePath, dSYMType string, numberOfDSYMs int) error { + for i := 0; i < numberOfDSYMs; i++ { + err := os.WriteFile(createDSYMFilePath(archivePath, dSYMType, i), nil, 0777) + if err != nil { + return err + } + } + + return nil +} + +func createDSYMFilePath(archivePath, dSYMType string, index int) string { + return filepath.Join(archivePath, DSYMSDirName, fmt.Sprintf("ios-%d.%s.dSYM", index, dSYMType)) +} + +func createArchive(archivePath string) (string, error) { + tempDirPath, err := pathutil.NormalizedOSTempDirPath(tempDirName) + if err != nil { + return "", err + } + + archivePath = filepath.Join(tempDirPath, archivePath) + err = os.MkdirAll(filepath.Join(archivePath, DSYMSDirName), 0755) + if err != nil { + return "", err + } + + return archivePath, nil +}