diff --git a/Tasks/XcodeV5/Strings/resources.resjson/en-US/resources.resjson b/Tasks/XcodeV5/Strings/resources.resjson/en-US/resources.resjson index a67f18019279..0d913cca3f81 100644 --- a/Tasks/XcodeV5/Strings/resources.resjson/en-US/resources.resjson +++ b/Tasks/XcodeV5/Strings/resources.resjson/en-US/resources.resjson @@ -24,6 +24,8 @@ "loc.input.help.xcodeDeveloperDir": "(Optional) Enter a path to a specific Xcode developer directory (e.g. `/Applications/Xcode_9.0.0.app/Contents/Developer`). This is useful when multiple versions of Xcode are installed on the agent machine.", "loc.input.label.packageApp": "Create app package", "loc.input.help.packageApp": "Indicate whether an IPA app package file should be generated as a part of the build.", + "loc.input.label.skipBuildStep": "Skip build step", + "loc.input.help.skipBuildStep": "Skip the Xcode build step and go directly to package creation. This is useful when you have a pre-built archive and only need to export it.", "loc.input.label.archivePath": "Archive path", "loc.input.help.archivePath": "(Optional) Specify a directory where created archives should be placed.", "loc.input.label.exportPath": "Export path", diff --git a/Tasks/XcodeV5/Tests/L0.ts b/Tasks/XcodeV5/Tests/L0.ts index 04d002aeb890..8acff94410f0 100644 --- a/Tasks/XcodeV5/Tests/L0.ts +++ b/Tasks/XcodeV5/Tests/L0.ts @@ -700,4 +700,29 @@ describe('Xcode L0 Suite', function () { assert(tr.succeeded, 'task should have succeeded'); assert(tr.invokedToolCount === 6, 'Should have ran 6 command lines.'); }); + + it('Skip build step when skipBuildStep is enabled', async function () { + let tp = path.join(__dirname, 'L0SkipBuildStep.js'); + let tr: ttm.MockTestRunner = new ttm.MockTestRunner(tp); + + await tr.runAsync(); + + // Should run version check + assert(tr.ran('/home/bin/xcodebuild -version'), 'xcodebuild for version should have been run.'); + + // Should NOT run the initial build step when skipBuildStep is enabled + assert(!tr.ran('/home/bin/xcodebuild -sdk iphoneos -configuration Release -workspace /user/build/MyApp.xcodeproj/project.xcworkspace -scheme MyScheme build CODE_SIGNING_ALLOWED=NO'), + 'Initial build should have been skipped when skipBuildStep is enabled.'); + + // Should run the archive step in the packaging section + assert(tr.ran('/home/bin/xcodebuild -workspace /user/build/MyApp.xcodeproj/project.xcworkspace -scheme MyScheme archive -sdk iphoneos -configuration Release -archivePath /user/build/MyScheme CODE_SIGNING_ALLOWED=NO'), + 'xcodebuild archive should have been run in the packaging section.'); + + // Should run the export step + assert(tr.ran('/home/bin/xcodebuild -exportArchive -archivePath /user/build/MyScheme.xcarchive -exportPath /user/build -exportOptionsPlist _XcodeTaskExportOptions.plist'), + 'xcodebuild exportArchive should have been run to export the IPA from the .xcarchive'); + + assert(tr.stderr.length === 0, 'should not have written to stderr'); + assert(tr.succeeded, 'task should have succeeded'); + }); }); \ No newline at end of file diff --git a/Tasks/XcodeV5/Tests/L0SkipBuildStep.ts b/Tasks/XcodeV5/Tests/L0SkipBuildStep.ts new file mode 100644 index 000000000000..70093a80a13b --- /dev/null +++ b/Tasks/XcodeV5/Tests/L0SkipBuildStep.ts @@ -0,0 +1,88 @@ +import ma = require('azure-pipelines-task-lib/mock-answer'); +import tmrm = require('azure-pipelines-task-lib/mock-run'); +import path = require('path'); + +let taskPath = path.join(__dirname, '..', 'xcode.js'); +let tr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath); + +process.env['HOME'] = '/users/test'; +process.env['AGENT_VERSION'] = '2.122.0'; +process.env['BUILD_SOURCESDIRECTORY'] = '/user/build'; +process.env['SYSTEM_DEFAULTWORKINGDIRECTORY'] = '/user/build'; + +// Set task inputs +tr.setInput('actions', 'build'); +tr.setInput('configuration', 'Release'); +tr.setInput('sdk', 'iphoneos'); +tr.setInput('xcWorkspacePath', '**/*.xcodeproj/*.xcworkspace'); +tr.setInput('scheme', 'MyScheme'); +tr.setInput('xcodeVersion', 'default'); +tr.setInput('xcodeDeveloperDir', ''); +tr.setInput('packageApp', 'true'); +tr.setInput('skipBuildStep', 'true'); // This is the key difference +tr.setInput('signingOption', 'nosign'); +tr.setInput('args', ''); +tr.setInput('cwd', '/user/build'); +tr.setInput('destinationPlatformOption', 'default'); +tr.setInput('outputPattern', ''); +tr.setInput('useXcpretty', 'false'); +tr.setInput('publishJUnitResults', 'false'); +tr.setInput('archivePath', '/user/build'); +tr.setInput('exportPath', '/user/build'); +tr.setInput('exportOptions', 'auto'); + +// Provide mock answers for task execution +let a: ma.TaskLibAnswers = { + "which": { + "xcodebuild": "/home/bin/xcodebuild", + "/usr/libexec/PlistBuddy": "/usr/libexec/PlistBuddy" + }, + "checkPath": { + "/home/bin/xcodebuild": true, + "/usr/libexec/PlistBuddy": true + }, + "filePathSupplied": { + "xcWorkspacePath": true, + "archivePath": false + }, + "getVariable": { + "build.sourcesDirectory": "/user/build", + "HOME": "/users/test" + }, + "stats": { + "/user/build": { + "isFile": false + } + }, + "findMatch": { + "**/*.xcodeproj/*.xcworkspace": [ + "/user/build/MyApp.xcodeproj/project.xcworkspace" + ], + "**/*.xcarchive": [ + "/user/build/MyScheme.xcarchive" + ] + }, + "exec": { + "/home/bin/xcodebuild -version": { + "code": 0, + "stdout": "Xcode 12.4\nBuild version 12D4e" + }, + "/home/bin/xcodebuild -workspace /user/build/MyApp.xcodeproj/project.xcworkspace -scheme MyScheme archive -sdk iphoneos -configuration Release -archivePath /user/build/MyScheme CODE_SIGNING_ALLOWED=NO": { + "code": 0, + "stdout": "archive step completed successfully" + }, + "/home/bin/xcodebuild -exportArchive -archivePath /user/build/MyScheme.xcarchive -exportPath /user/build -exportOptionsPlist _XcodeTaskExportOptions.plist": { + "code": 0, + "stdout": "exportArchive completed successfully" + }, + "/usr/libexec/PlistBuddy -c Clear _XcodeTaskExportOptions.plist": { + "code": 0, + "stdout": "plist cleared" + } + } +}; + +tr.setAnswers(a); + +// Run the task +tr.run(); \ No newline at end of file diff --git a/Tasks/XcodeV5/task.json b/Tasks/XcodeV5/task.json index 843e653fbbd8..e9d2bb3c6a1c 100644 --- a/Tasks/XcodeV5/task.json +++ b/Tasks/XcodeV5/task.json @@ -13,7 +13,7 @@ "version": { "Major": 5, "Minor": 248, - "Patch": 1 + "Patch": 2 }, "releaseNotes": "This version of the task is compatible with Xcode 8 - 13. Features that were solely to maintain compatibility with Xcode 7 have been removed. This task has better options for using Microsoft-hosted macOS agents.", "demands": [ @@ -118,6 +118,16 @@ "helpMarkDown": "Indicate whether an IPA app package file should be generated as a part of the build.", "groupName": "package" }, + { + "name": "skipBuildStep", + "type": "boolean", + "label": "Skip build step", + "defaultValue": false, + "required": false, + "helpMarkDown": "Skip the Xcode build step and go directly to package creation. This is useful when you have a pre-built archive and only need to export it.", + "groupName": "package", + "visibleRule": "packageApp == true" + }, { "name": "archivePath", "type": "filePath", diff --git a/Tasks/XcodeV5/task.loc.json b/Tasks/XcodeV5/task.loc.json index c395e5d53a60..f1bb4e5d3984 100644 --- a/Tasks/XcodeV5/task.loc.json +++ b/Tasks/XcodeV5/task.loc.json @@ -13,7 +13,7 @@ "version": { "Major": 5, "Minor": 248, - "Patch": 1 + "Patch": 2 }, "releaseNotes": "ms-resource:loc.releaseNotes", "demands": [ @@ -118,6 +118,16 @@ "helpMarkDown": "ms-resource:loc.input.help.packageApp", "groupName": "package" }, + { + "name": "skipBuildStep", + "type": "boolean", + "label": "ms-resource:loc.input.label.skipBuildStep", + "defaultValue": false, + "required": false, + "helpMarkDown": "ms-resource:loc.input.help.skipBuildStep", + "groupName": "package", + "visibleRule": "packageApp == true" + }, { "name": "archivePath", "type": "filePath", diff --git a/Tasks/XcodeV5/xcode.ts b/Tasks/XcodeV5/xcode.ts index d3f7555a1122..7e5b11c04f2e 100644 --- a/Tasks/XcodeV5/xcode.ts +++ b/Tasks/XcodeV5/xcode.ts @@ -127,10 +127,12 @@ async function run() { let useXcpretty: boolean = tl.getBoolInput('useXcpretty', false); let actions: string[] = tl.getDelimitedInput('actions', ' ', true); let packageApp: boolean = tl.getBoolInput('packageApp', true); + let skipBuildStep: boolean = tl.getBoolInput('skipBuildStep', false); let args: string = tl.getInput('args', false); telemetryData.actions = actions; telemetryData.packageApp = packageApp; + telemetryData.skipBuildStep = skipBuildStep; //-------------------------------------------------------- // Exec Tools @@ -261,21 +263,35 @@ async function run() { } //--- 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; - } - }); + // Determine if we should skip the initial build step + const isArchiveOnlyAction = actions.length === 1 && actions[0] === 'archive'; + let skipInitialBuild = false; + if ((isArchiveOnlyAction && packageApp && sdk !== 'iphonesimulator') || (skipBuildStep && packageApp)) { + // If only "archive" is requested and packageApp is true, skip the initial build + // OR if skipBuildStep is explicitly enabled and packageApp is true + skipInitialBuild = true; + tl.debug('Skipping initial build since only "archive" action is specified and packageApp is true, or skipBuildStep is enabled.'); + } + + if (!skipInitialBuild) { + 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')); + 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; } - throw err; + } else { + tl.debug('Skipping Xcode build step as requested.'); } //--------------------------------------------------------