import * as core from '@actions/core' ; import * as gha_exec from '@actions/exec' ; import { spawnSync } from 'child_process' ; import type { SpawnSyncOptions } from 'child_process' ; import * as path from 'path' ; import * as fs from 'fs' ; import semver , { Range } from 'semver' ; import type { SemVer } from 'semver' ; async function mdls ( path ) { const v = await exec ( 'mdls' , [ '-raw' , '-name' , 'kMDItemVersion' , path ] ) ; if ( core . getInput ( 'verbosity' ) == 'verbose' ) { process . stdout . write ( '\n' ) ; } return semver . coerce ( v ) ?? undefined ; } async function xcodes ( ) { const paths = ( await exec ( 'mdfind' , [ 'kMDItemCFBundleIdentifier = com.apple.dt.Xcode' ] ) ) . split ( '\n' ) ; const rv = [ ] ; for ( const path of paths ) { if ( ! path . trim ( ) ) continue ; const v = await mdls ( path ) ; if ( v ) { rv . push ( [ path , v ] ) ; } } return rv ; } export function spawn ( arg0 , args , options = { stdio : 'inherit' } ) { const { error , signal , status } = spawnSync ( arg0 , args , options ) ; if ( error ) throw error ; if ( signal ) throw new Error ( `\`${ arg0 }\` terminated with signal (${ signal })` ) ; if ( status != 0 ) throw new Error ( `\`${ arg0 }\` aborted (${ status })` ) ; } export async function xcselect ( xcode ? , swift ? ) { if ( swift ) { return selectSwift ( swift ) ; } else if ( xcode ) { return selectXcode ( xcode ) ; } const gotDotSwiftVersion = dotSwiftVersion ( ) ; if ( gotDotSwiftVersion ) { core . info ( `» \`.swift-version\` » ~> ${ gotDotSwiftVersion }` ) ; return selectSwift ( gotDotSwiftVersion ) ; } else { const devdir = await exec ( 'xcode-select' , [ '--print-path' ] ) ; const xcodePath = path . dirname ( path . dirname ( devdir ) ) ; const version = await mdls ( xcodePath ) ; if ( version ) { return version ; } else { return selectXcode ( ) ; } } async function selectXcode ( range ? ) { const rv = ( await xcodes ( ) ) . filter ( ( [ , v ] ) => ( range ? semver . satisfies ( v , range ) : true ) ) . sort ( ( a , b ) => semver . compare ( a [ 1 ] , b [ 1 ] ) ) . pop ( ) ; if ( ! rv ) throw new Error ( `No Xcode ~> ${ range }` ) ; spawn ( 'sudo' , [ 'xcode-select' , '--switch' , rv [ 0 ] ] ) ; return rv [ 1 ] ; } async function selectSwift ( range ) { const rv1 = await xcodes ( ) ; const rv2 = await Promise . all ( rv1 . map ( swiftVersion ) ) ; const rv3 = rv2 . filter ( ( [ , , sv ] ) => semver . satisfies ( sv , range ) ) . sort ( ( a , b ) => semver . compare ( a [ 1 ] , b [ 1 ] ) ) . pop ( ) ; if ( ! rv3 ) throw new Error ( `No Xcode with Swift ~> ${ range }` ) ; core . info ( `» Selected Swift ${ rv3 [ 2 ] }` ) ; spawn ( 'sudo' , [ 'xcode-select' , '--switch' , rv3 [ 0 ] ] ) ; return rv3 [ 1 ] ; async function swiftVersion ( [ DEVELOPER_DIR , xcodeVersion ] ) { const stdout = await exec ( 'swift' , [ '--version' ] , { DEVELOPER_DIR } , false ) ; const matches = stdout . match ( /Swift version (.+?)\s/m ) ; if ( ! matches || ! matches [ 1 ] ) throw new Error ( `failed to extract Swift version from Xcode ${ xcodeVersion }` ) ; const version = semver . coerce ( matches [ 1 ] ) ; if ( ! version ) throw new Error ( `failed to parse Swift version from Xcode ${ xcodeVersion }` ) ; return [ DEVELOPER_DIR , xcodeVersion , version ] ; } } function dotSwiftVersion ( ) { if ( ! fs . existsSync ( '.swift-version' ) ) return undefined ; const version = fs . readFileSync ( '.swift-version' ) . toString ( ) . trim ( ) ; try { return new Range ( '~' + version ) ; } catch ( error ) { core . warning ( `Failed to parse Swift version from .swift-version: ${ error }` ) ; } } } interface Devices { devices : { [ key ] : [ { udid : string ; } ] ; } ; } type DeviceType = 'watchOS' | 'tvOS' | 'iOS' ; type Destination = { [ key ] : string ; } ; interface Schemes { workspace ? : { schemes : string [ ] ; } ; project ? : { schemes : string [ ] ; } ; } export async function getSchemeFromPackage ( ) { const out = await exec ( 'xcodebuild' , [ '-list' , '-json' ] ) ; const json = parseJSON < Schemes > ( out ) ; const schemes = ( json ?. workspace ?? json ?. project ) ?. schemes ; if ( ! schemes || schemes . length == 0 ) throw new Error ( 'Could not determine scheme' ) ; for ( const scheme of schemes ) { if ( scheme . endsWith ( '-Package' ) ) return scheme ; } return schemes [ 0 ] ; } function parseJSON < T > ( input ) { try { input = input . trim ( ) ; const xcodebuildSucks = 'build session not created after 15 seconds - still waiting' ; if ( input . endsWith ( xcodebuildSucks ) ) { input = input . slice ( 0 , - xcodebuildSucks . length ) ; } return JSON . parse ( input ) as T ; } catch ( error ) { core . startGroup ( 'JSON' ) ; core . error ( input ) ; core . endGroup ( ) ; throw error ; } } async function destinations ( ) { const out = await exec ( 'xcrun' , [ 'simctl' , 'list' , '--json' , 'devices' , 'available' , ] ) ; const devices = parseJSON < Devices > ( out ) . devices ; const rv = { } ; for ( const opaqueIdentifier in devices ) { const device = ( devices [ opaqueIdentifier ] ?? [ ] ) [ 0 ] ; if ( ! device ) continue ; const [ type , v ] = parse ( opaqueIdentifier ) ; if ( v && ( ! rv [ type ] || semver . lt ( rv [ type ] . v , v ) ) ) { rv [ type ] = { v , id : device . udid } ; } } return { tvOS : rv . tvOS ?. id , watchOS : rv . watchOS ?. id , iOS : rv . iOS ?. id , } ; function parse ( key ) { const [ type , ... vv ] = ( key . split ( '.' ) . pop ( ) ?? '' ) . split ( '-' ) ; const v = semver . coerce ( vv . join ( '.' ) ) ; return [ type as DeviceType , v ?? undefined ] ; } } async function exec ( command , args ? , env ? , stdErrToWarning = true ) { let out = '' ; try { await gha_exec . exec ( command , args , { listeners : { stdout : ( data ) => ( out += data . toString ( ) ) , stderr : ( data ) => { const message = `${ command }: ${ '\u001b[33m' }${ data . toString ( ) }` ; if ( stdErrToWarning ) { core . warning ( message ) ; } else { core . info ( message ) ; } } , } , silent : verbosity ( ) != 'verbose' , env , } ) ; return out ; } catch ( error ) { core . info ( `» ${ command } ${ args ? args . join ( ' \\\n' ) : '' }` ) ; throw error ; } } export function verbosity ( ) { const value = core . getInput ( 'verbosity' ) ; switch ( value ) { case 'xcpretty' : case 'quiet' : case 'verbose' : return value ; default : if ( core . getBooleanInput ( 'quiet' ) ) return 'quiet' ; core . warning ( `invalid value for \`verbosity\` (${ value })` ) ; return 'xcpretty' ; } } export function getConfiguration ( ) { const conf = core . getInput ( 'configuration' ) ; switch ( conf ) { case 'debug' : return 'Debug' ; case 'release' : return 'Release' ; default : return conf ; } } export type Platform = 'watchOS' | 'iOS' | 'tvOS' | 'macOS' | 'mac-catalyst' ; export function getAction ( xcodeVersion , platform ? ) { const action = core . getInput ( 'action' ) ; if ( platform == 'watchOS' && actionIsTestable ( action ) && semver . lt ( xcodeVersion , '12.5.0' ) ) { core . notice ( 'Setting `action=build` for Apple Watch / Xcode <12.5' ) ; return 'build' ; } return action ?? undefined ; } export function actionIsTestable ( action ? ) { return action == 'test' || action == 'build-for-testing' ; } export async function getDestination ( xcodeVersion , platform ? ) { switch ( platform ) { case 'iOS' : case 'tvOS' : case 'watchOS' : { const id = ( await destinations ( ) ) [ platform ] ; return [ '-destination' , `id=${ id }` ] ; } case 'macOS' : return [ '-destination' , 'platform=macOS' ] ; case 'mac-catalyst' : return [ '-destination' , 'platform=macOS,variant=Mac Catalyst' ] ; case undefined : if ( semver . gte ( xcodeVersion , '13.0.0' ) ) { return [ '-destination' , 'platform=macOS' ] ; } else { return [ ] ; } default : throw new Error ( `Invalid platform: ${ platform }` ) ; } } export function getIdentity ( identity , platform ? ) { if ( identity ) { return `CODE_SIGN_IDENTITY="${ identity }"` ; } if ( platform == 'mac-catalyst' ) { core . notice ( 'Disabling code signing for Mac Catalyst.' ) ; return 'CODE_SIGN_IDENTITY=-' ; } } function security ( ... args ) { for ( const arg of args ) { if ( arg . includes ( '\n' ) ) throw new Error ( 'Invalid security argument' ) ; } const command = args . join ( ' ' ) . concat ( '\n' ) ; spawn ( '/usr/bin/security' , [ '-i' ] , { input : command } ) ; } export async function createKeychain ( certificate , passphrase ) { core . setSecret ( certificate ) ; core . setSecret ( passphrase ) ; const password = ( await exec ( '/usr/bin/uuidgen' ) ) . trim ( ) ; core . setSecret ( password ) ; const name = ( await exec ( '/usr/bin/uuidgen' ) ) . trim ( ) ; core . setSecret ( name ) ; const keychainPath = `${ process . env . RUNNER_TEMP }/${ name }.keychain-db` ; core . saveState ( 'keychainPath' , keychainPath ) ; const keychainSearchPath = ( await exec ( '/usr/bin/security' , [ 'list-keychains' , '-d' , 'user' ] ) ) . split ( '\n' ) . map ( ( value ) => value . trim ( ) ) ; core . saveState ( 'keychainSearchPath' , keychainSearchPath ) ; core . info ( 'Creating keychain' ) ; security ( 'create-keychain' , '-p' , password , keychainPath ) ; security ( 'set-keychain-settings' , '-lut' , '21600' , keychainPath ) ; security ( 'unlock-keychain' , '-p' , password , keychainPath ) ; core . info ( 'Importing certificate to keychain' ) ; const certificatePath = `${ process . env . RUNNER_TEMP }/${ name }.p12` ; fs . writeFileSync ( certificatePath , certificate , { encoding : 'base64' } ) ; try { security ( 'import' , certificatePath , '-P' , passphrase , '-A' , '-t' , 'cert' , '-f' , 'pkcs12' , '-x' , '-k' , keychainPath ) ; } finally { fs . unlinkSync ( certificatePath ) ; } core . info ( 'Updating keychain search path' ) ; security ( 'list-keychains' , '-d' , 'user' , '-s' , keychainPath , ... keychainSearchPath ) ; } export function deleteKeychain ( ) { const state = core . getState ( 'keychainSearchPath' ) ; if ( state ) { const keychainSearchPath = JSON . parse ( state ) ; core . info ( 'Restoring keychain search path' ) ; try { security ( 'list-keychains' , '-d' , 'user' , '-s' , ... keychainSearchPath ) ; } catch ( error ) { core . error ( 'Failed to restore keychain search path: ' + error ) ; } } const keychainPath = core . getState ( 'keychainPath' ) ; if ( keychainPath ) { core . info ( 'Deleting keychain' ) ; try { security ( 'delete-keychain' , keychainPath ) ; } catch ( error ) { core . error ( 'Failed to delete keychain: ' + error ) ; if ( fs . existsSync ( keychainPath ) ) { fs . unlinkSync ( keychainPath ) ; } } } }