in Tasks/XcodeV5/xcode.ts [9:505]
async function run() {
const telemetryData: { [key: string]: any; } = {};
try {
tl.setResourcePath(path.join(__dirname, 'task.json'));
//--------------------------------------------------------
// Tooling
//--------------------------------------------------------
let xcodeVersionSelection: string = tl.getInput('xcodeVersion', true);
telemetryData.xcodeVersionSelection = xcodeVersionSelection;
if (xcodeVersionSelection === 'specifyPath') {
let devDir = tl.getInput('xcodeDeveloperDir', true);
tl.setVariable('DEVELOPER_DIR', devDir);
if (devDir) {
telemetryData.xcodeFileName = utils.getXcodeFileName(devDir);
}
}
else if (xcodeVersionSelection !== 'default') {
// resolve the developer dir for a version like "8" or "9".
let devDir = utils.findDeveloperDir(xcodeVersionSelection);
tl.setVariable('DEVELOPER_DIR', devDir);
}
let tool: string = tl.which('xcodebuild', true);
tl.debug('Tool selected: ' + tool);
let workingDir: string = tl.getPathInput('cwd');
tl.cd(workingDir);
//--------------------------------------------------------
// Xcode args
//--------------------------------------------------------
let ws: string = tl.getPathInput('xcWorkspacePath', false, false);
if (tl.filePathSupplied('xcWorkspacePath')) {
let workspaceMatches = tl.findMatch(workingDir, ws, { allowBrokenSymbolicLinks: false, followSpecifiedSymbolicLink: false, followSymbolicLinks: false });
tl.debug("Found " + workspaceMatches.length + ' workspaces matching.');
if (workspaceMatches.length > 0) {
ws = workspaceMatches[0];
if (workspaceMatches.length > 1) {
tl.warning(tl.loc('MultipleWorkspacesFound', ws));
}
}
else {
throw new Error(tl.loc('WorkspaceDoesNotExist', ws));
}
}
let isProject = false;
if (ws && ws.trim().toLowerCase().endsWith('.xcodeproj')) {
isProject = true;
}
let scheme: string = tl.getInput('scheme', false);
// If we have a workspace argument but no scheme, see if there's
// single shared scheme we can use.
if (!scheme && !isProject && ws && tl.filePathSupplied('xcWorkspacePath')) {
try {
let schemes: string[] = await utils.getWorkspaceSchemes(tool, ws);
if (schemes.length > 1) {
tl.warning(tl.loc('MultipleSchemesFound'));
}
else if (schemes.length === 0) {
tl.warning(tl.loc('NoSchemeFound'));
}
else {
scheme = schemes[0];
console.log(tl.loc('SchemeSelected', scheme));
}
}
catch (err) {
tl.warning(tl.loc('FailedToFindScheme'));
}
}
let destinations: string[];
let platform: string = tl.getInput('destinationPlatformOption', false);
if (platform === 'custom') {
// Read the custom platform from the text input.
platform = tl.getInput('destinationPlatform', false);
}
if (platform === 'macOS') {
destinations = ['platform=macOS'];
}
else if (platform && platform !== 'default') {
// To be yaml friendly, destinationTypeOption is optional and we default to simulators.
let destinationType: string = tl.getInput('destinationTypeOption', false);
let targetingSimulators: boolean = destinationType !== 'devices';
let devices: string[];
if (targetingSimulators) {
let simulator = tl.getInput('destinationSimulators');
if(!simulator){
simulator = utils.getDefaultSimulator(platform, xcodeVersionSelection);
console.log(tl.loc('UsingDefaultSimulator', simulator));
}
// Only one simulator for now.
devices = [simulator];
}
else {
// Only one device for now.
devices = [tl.getInput('destinationDevices')];
}
destinations = utils.buildDestinationArgs(platform, devices, targetingSimulators);
}
let sdk: string = tl.getInput('sdk', false);
let configuration: string = tl.getInput('configuration', false);
let useXcpretty: boolean = tl.getBoolInput('useXcpretty', false);
let actions: string[] = tl.getDelimitedInput('actions', ' ', true);
let packageApp: boolean = tl.getBoolInput('packageApp', true);
let args: string = tl.getInput('args', false);
telemetryData.actions = actions;
telemetryData.packageApp = packageApp;
//--------------------------------------------------------
// Exec Tools
//--------------------------------------------------------
// --- Xcode Version ---
let xcv: ToolRunner = tl.tool(tool);
xcv.arg('-version');
let xcodeMajorVersion: number = 0;
xcv.on('stdout', (data) => {
const match = data.toString().trim().match(/Xcode (.+)/g);
tl.debug('match = ' + match);
if (match) {
const versionString = match.toString().replace('Xcode', '').trim();
const majorVersion: number = parseInt(versionString);
tl.debug('majorVersion = ' + majorVersion);
telemetryData.xcodeVersion = versionString;
if (!isNaN(majorVersion)) {
xcodeMajorVersion = majorVersion;
}
}
});
await xcv.exec();
tl.debug('xcodeMajorVersion = ' + xcodeMajorVersion);
// --- Xcode build arguments ---
let xcb: ToolRunner = tl.tool(tool);
xcb.argIf(sdk, ['-sdk', sdk]);
xcb.argIf(configuration, ['-configuration', configuration]);
if (ws && tl.filePathSupplied('xcWorkspacePath')) {
xcb.argIf(isProject, '-project');
xcb.argIf(!isProject, '-workspace');
xcb.arg(ws);
}
xcb.argIf(scheme, ['-scheme', scheme]);
// Add a -destination argument for each device and simulator.
if (destinations) {
destinations.forEach(destination => {
xcb.arg(['-destination', destination]);
});
}
xcb.arg(actions);
if (args) {
xcb.line(args);
}
//--------------------------------------------------------
// iOS signing and provisioning
//--------------------------------------------------------
let signingOption: string = tl.getInput('signingOption', true);
let xcode_codeSigningAllowed: string;
let xcode_codeSignStyle: string;
let xcode_otherCodeSignFlags: string;
let xcode_codeSignIdentity: string;
let xcode_provProfile: string;
let xcode_provProfileSpecifier: string;
let xcode_devTeam: string;
telemetryData.signingOption = signingOption;
if (signingOption === 'nosign') {
xcode_codeSigningAllowed = 'CODE_SIGNING_ALLOWED=NO';
}
else if (signingOption === 'manual') {
xcode_codeSignStyle = 'CODE_SIGN_STYLE=Manual';
const signIdentity: string = tl.getInput('signingIdentity');
if (signIdentity) {
xcode_codeSignIdentity = 'CODE_SIGN_IDENTITY=' + signIdentity;
}
let provProfileUUID: string = tl.getInput('provisioningProfileUuid');
let provProfileName: string = tl.getInput('provisioningProfileName');
if (!provProfileUUID) {
provProfileUUID = "";
}
if (!provProfileName) {
provProfileName = "";
}
// PROVISIONING_PROFILE_SPECIFIER takes predence over PROVISIONING_PROFILE,
// so it's important to pass it to Xcode even if it's empty. That way Xcode
// will ignore any specifier in the project file and honor the specifier
// or uuid we passed on the commandline. If the user wants to use the specifier
// in the project file, they should choose the "Project Defaults" signing style.
xcode_provProfile = `PROVISIONING_PROFILE=${provProfileUUID}`;
xcode_provProfileSpecifier = `PROVISIONING_PROFILE_SPECIFIER=${provProfileName}`;
}
else if (signingOption === 'auto') {
xcode_codeSignStyle = 'CODE_SIGN_STYLE=Automatic';
let teamId: string = tl.getInput('teamId');
if (teamId) {
xcode_devTeam = 'DEVELOPMENT_TEAM=' + teamId;
}
}
xcb.argIf(xcode_codeSigningAllowed, xcode_codeSigningAllowed);
xcb.argIf(xcode_codeSignStyle, xcode_codeSignStyle);
xcb.argIf(xcode_codeSignIdentity, xcode_codeSignIdentity);
xcb.argIf(xcode_provProfile, xcode_provProfile);
xcb.argIf(xcode_provProfileSpecifier, xcode_provProfileSpecifier);
xcb.argIf(xcode_devTeam, xcode_devTeam);
//--- Enable Xcpretty formatting ---
if (useXcpretty && !tl.which('xcpretty')) {
// user wants to enable xcpretty but it is not installed, fallback to xcodebuild raw output
useXcpretty = false;
tl.warning(tl.loc("XcprettyNotInstalled"));
}
if (useXcpretty) {
let xcPrettyPath: string = tl.which('xcpretty', true);
let xcPrettyTool: ToolRunner = tl.tool(xcPrettyPath);
xcPrettyTool.arg(['-r', 'junit', '--no-color']);
const xcPrettyArgs: string = tl.getInput('xcprettyArgs');
if (xcPrettyArgs) {
xcPrettyTool.line(xcPrettyArgs);
}
const logFile: string = utils.getUniqueLogFileName('xcodebuild');
xcb.pipeExecOutputToTool(xcPrettyTool, logFile);
utils.setTaskState('XCODEBUILD_LOG', logFile);
}
//--- Xcode Build ---
let buildOnlyDeviceErrorFound: boolean;
xcb.on('errline', (line: string) => {
if (!buildOnlyDeviceErrorFound && line.includes('build only device cannot be used to run this target')) {
buildOnlyDeviceErrorFound = true;
}
});
try {
await xcb.exec();
} catch (err) {
if (buildOnlyDeviceErrorFound) {
// Tell the user they need to change Destination platform to fix this build error.
tl.warning(tl.loc('NoDestinationPlatformWarning'));
}
throw err;
}
//--------------------------------------------------------
// Package app to generate .ipa
//--------------------------------------------------------
if (packageApp && sdk !== 'iphonesimulator') {
// use xcodebuild to create the app package
if (!scheme) {
throw new Error(tl.loc("SchemeRequiredForArchive"));
}
if (!ws || !tl.filePathSupplied('xcWorkspacePath')) {
throw new Error(tl.loc("WorkspaceOrProjectRequiredForArchive"));
}
// create archive
let xcodeArchive: ToolRunner = tl.tool(tl.which('xcodebuild', true));
if (ws && tl.filePathSupplied('xcWorkspacePath')) {
xcodeArchive.argIf(isProject, '-project');
xcodeArchive.argIf(!isProject, '-workspace');
xcodeArchive.arg(ws);
}
xcodeArchive.argIf(scheme, ['-scheme', scheme]);
xcodeArchive.arg('archive'); //archive action
xcodeArchive.argIf(sdk, ['-sdk', sdk]);
xcodeArchive.argIf(configuration, ['-configuration', configuration]);
let archivePath: string = tl.getInput('archivePath');
let archiveFolderRoot: string;
if (!archivePath.endsWith('.xcarchive')) {
archiveFolderRoot = archivePath;
archivePath = tl.resolve(archivePath, scheme);
} else {
//user specified a file path for archivePath
archiveFolderRoot = path.dirname(archivePath);
}
xcodeArchive.arg(['-archivePath', archivePath]);
xcodeArchive.argIf(xcode_otherCodeSignFlags, xcode_otherCodeSignFlags);
xcodeArchive.argIf(xcode_codeSigningAllowed, xcode_codeSigningAllowed);
xcodeArchive.argIf(xcode_codeSignStyle, xcode_codeSignStyle);
xcodeArchive.argIf(xcode_codeSignIdentity, xcode_codeSignIdentity);
xcodeArchive.argIf(xcode_provProfile, xcode_provProfile);
xcodeArchive.argIf(xcode_provProfileSpecifier, xcode_provProfileSpecifier);
xcodeArchive.argIf(xcode_devTeam, xcode_devTeam);
if (args) {
xcodeArchive.line(args);
}
if (useXcpretty) {
let xcPrettyTool: ToolRunner = tl.tool(tl.which('xcpretty', true));
xcPrettyTool.arg('--no-color');
const xcPrettyArgs: string = tl.getInput('xcprettyArgs');
if (xcPrettyArgs) {
xcPrettyTool.line(xcPrettyArgs);
}
const logFile: string = utils.getUniqueLogFileName('xcodebuild_archive');
xcodeArchive.pipeExecOutputToTool(xcPrettyTool, logFile);
utils.setTaskState('XCODEBUILD_ARCHIVE_LOG', logFile);
}
await xcodeArchive.exec();
let archiveFolders: string[] = tl.findMatch(archiveFolderRoot, '**/*.xcarchive', { allowBrokenSymbolicLinks: false, followSpecifiedSymbolicLink: false, followSymbolicLinks: false });
if (archiveFolders && archiveFolders.length > 0) {
tl.debug(archiveFolders.length + ' archives found for exporting.');
//export options plist
let exportOptions: string = tl.getInput('exportOptions');
let exportMethod: string;
let exportTeamId: string;
let exportOptionsPlist: string;
let archiveToCheck: string = archiveFolders[0];
let macOSEmbeddedProfilesFound: boolean;
telemetryData.exportOptions = exportOptions;
// iOS provisioning profiles use the .mobileprovision suffix. macOS profiles have the .provisionprofile suffix.
let embeddedProvProfiles: string[] = tl.findMatch(archiveToCheck, '**/embedded.mobileprovision', { allowBrokenSymbolicLinks: false, followSpecifiedSymbolicLink: false, followSymbolicLinks: false });
if (embeddedProvProfiles && embeddedProvProfiles.length > 0) {
tl.debug(`${embeddedProvProfiles.length} iOS embedded.mobileprovision file(s) found.`);
} else {
embeddedProvProfiles = tl.findMatch(archiveToCheck, '**/embedded.provisionprofile', { allowBrokenSymbolicLinks: false, followSpecifiedSymbolicLink: false, followSymbolicLinks: false });
if (embeddedProvProfiles && embeddedProvProfiles.length > 0) {
tl.debug(`${embeddedProvProfiles.length} macOS embedded.provisionprofile file(s) found.`);
macOSEmbeddedProfilesFound = true;
}
}
if (exportOptions === 'auto') {
// Automatically try to detect the export-method to use from the provisioning profile
// embedded in the .xcarchive file
if (embeddedProvProfiles && embeddedProvProfiles.length > 0) {
tl.debug('embedded prov profile = ' + embeddedProvProfiles[0]);
if (macOSEmbeddedProfilesFound) {
exportMethod = await sign.getmacOSProvisioningProfileType(embeddedProvProfiles[0]);
} else {
exportMethod = await sign.getiOSProvisioningProfileType(embeddedProvProfiles[0]);
}
tl.debug('Using export method = ' + exportMethod);
}
// If you create a simple macOS app with automatic signing and no entitlements, it won't have an embedded profile.
// And export for that app will work with an empty exportOptionsPlist.
if (!exportMethod && sdk && sdk !== 'macosx') {
tl.warning(tl.loc('ExportMethodNotIdentified'));
}
} else if (exportOptions === 'specify') {
exportMethod = tl.getInput('exportMethod', true);
exportTeamId = tl.getInput('exportTeamId');
} else if (exportOptions === 'plist') {
exportOptionsPlist = tl.getInput('exportOptionsPlist');
if (!tl.filePathSupplied('exportOptionsPlist') || !utils.pathExistsAsFile(exportOptionsPlist)) {
throw new Error(tl.loc('ExportOptionsPlistInvalidFilePath', exportOptionsPlist));
}
}
if (exportOptions !== 'plist') {
// As long as the user didn't provide a plist, start with an empty one.
// Xcode 7 warns "-exportArchive without -exportOptionsPlist is deprecated"
// Xcode 8+ will error if a plist isn't provided.
let plist: string = tl.which('/usr/libexec/PlistBuddy', true);
exportOptionsPlist = '_XcodeTaskExportOptions.plist';
tl.tool(plist).arg(['-c', 'Clear', exportOptionsPlist]).execSync();
// Add the teamId if provided.
if (exportTeamId) {
tl.tool(plist).arg(['-c', 'Add teamID string ' + exportTeamId, exportOptionsPlist]).execSync();
}
// Add the export method if provided or determined above.
if (exportMethod) {
tl.tool(plist).arg(['-c', 'Add method string ' + exportMethod, exportOptionsPlist]).execSync();
}
// For auto export, conditionally add entitlements, signingStyle and provisioning profiles.
if (xcodeMajorVersion >= 9 && exportOptions === 'auto') {
// Propagate any iCloud entitlement.
if (embeddedProvProfiles && embeddedProvProfiles.length > 0) {
const cloudEntitlement = await sign.getCloudEntitlement(embeddedProvProfiles[0], exportMethod);
if (cloudEntitlement) {
tl.debug("Adding cloud entitlement");
tl.tool(plist).arg(['-c', `Add iCloudContainerEnvironment string ${cloudEntitlement}`, exportOptionsPlist]).execSync();
}
}
let signingOptionForExport = signingOption;
// If we're using the project defaults, scan the pbxProject file for the type of signing being used.
if (signingOptionForExport === 'default') {
signingOptionForExport = await utils.getProvisioningStyle(ws);
if (!signingOptionForExport) {
tl.warning(tl.loc('CantDetermineProvisioningStyle'));
}
}
if (signingOptionForExport === 'manual') {
// Xcode 9 manual signing, set code sign style = manual
tl.tool(plist).arg(['-c', 'Add signingStyle string ' + 'manual', exportOptionsPlist]).execSync();
// add provisioning profiles to the exportOptions plist
// find bundle Id from Info.plist and prov profile name from the embedded profile in each .app package
tl.tool(plist).arg(['-c', 'Add provisioningProfiles dict', exportOptionsPlist]).execSync();
for (let i = 0; i < embeddedProvProfiles.length; i++) {
let embeddedProvProfile: string = embeddedProvProfiles[i];
let profileName: string = await sign.getProvisioningProfileName(embeddedProvProfile);
tl.debug('embedded provisioning profile = ' + embeddedProvProfile + ', profile name = ' + profileName);
let embeddedInfoPlist: string = tl.resolve(path.dirname(embeddedProvProfile), 'Info.plist');
let bundleId: string = await sign.getBundleIdFromPlist(embeddedInfoPlist);
tl.debug('embeddedInfoPlist path = ' + embeddedInfoPlist + ', bundle identifier = ' + bundleId);
if (!profileName || !bundleId) {
throw new Error(tl.loc('FailedToGenerateExportOptionsPlist'));
}
tl.tool(plist).arg(['-c', 'Add provisioningProfiles:' + bundleId + ' string ' + profileName, exportOptionsPlist]).execSync();
}
}
}
}
//export path
let exportPath: string = tl.getInput('exportPath');
for (let i = 0; i < archiveFolders.length; i++) {
let archive: string = archiveFolders.pop();
//export the archive
let xcodeExport: ToolRunner = tl.tool(tl.which('xcodebuild', true));
xcodeExport.arg(['-exportArchive', '-archivePath', archive]);
xcodeExport.arg(['-exportPath', exportPath]);
xcodeExport.argIf(exportOptionsPlist, ['-exportOptionsPlist', exportOptionsPlist]);
let exportArgs: string = tl.getInput('exportArgs');
xcodeExport.argIf(exportArgs, exportArgs);
if (useXcpretty) {
let xcPrettyTool: ToolRunner = tl.tool(tl.which('xcpretty', true));
xcPrettyTool.arg('--no-color');
const xcPrettyArgs: string = tl.getInput('xcprettyArgs');
if (xcPrettyArgs) {
xcPrettyTool.line(xcPrettyArgs);
}
const logFile: string = utils.getUniqueLogFileName('xcodebuild_export');
xcodeExport.pipeExecOutputToTool(xcPrettyTool, logFile);
utils.setTaskState('XCODEBUILD_EXPORT_LOG', logFile);
}
await xcodeExport.exec();
}
}
}
tl.setResult(tl.TaskResult.Succeeded, tl.loc('XcodeSuccess'));
}
catch (err) {
tl.setResult(tl.TaskResult.Failed, err);
}
finally {
// Publish telemetry
telemetry.emitTelemetry('TaskHub', 'Xcode', telemetryData);
}
}