Skip to content

Commit 6df784d

Browse files
author
Fatme
authored
Merge pull request #4206 from NativeScript/fatme/ios-deployment-target
fix: build correctly for iOS when IPHONEOS_DEPLOYMENT_TARGET = 11.0; is specified in build.xcconfig
2 parents 8d29c63 + a2a235c commit 6df784d

8 files changed

+243
-15
lines changed

lib/definitions/platform.d.ts

+15
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ interface IPlatformService extends IBuildPlatformAction, NodeJS.EventEmitter {
8383
*/
8484
shouldInstall(device: Mobile.IDevice, projectData: IProjectData, release: IRelease, outputPath?: string): Promise<boolean>;
8585

86+
/**
87+
*
88+
* @param {Mobile.IDevice} device The device where the application should be installed.
89+
* @param {IProjectData} projectData DTO with information about the project.
90+
* @param {string} @optional outputPath Directory containing build information and artifacts.
91+
*/
92+
validateInstall(device: Mobile.IDevice, projectData: IProjectData, release: IRelease, outputPath?: string): Promise<void>;
93+
8694
/**
8795
* Determines whether the project should undergo the prepare process.
8896
* @param {IShouldPrepareInfo} shouldPrepareInfo Options needed to decide whether to prepare.
@@ -307,6 +315,13 @@ interface INodeModulesDependenciesBuilder {
307315
interface IBuildInfo {
308316
prepareTime: string;
309317
buildTime: string;
318+
/**
319+
* Currently it is used only for iOS.
320+
* As `xcrun` command does not throw an error when IPHONEOS_DEPLOYMENT_TARGET is provided in `xcconfig` file and
321+
* the simulator's version does not match IPHONEOS_DEPLOYMENT_TARGET's value, we need to save it to buildInfo file
322+
* in order check it on livesync and throw an error to the user.
323+
*/
324+
deploymentTarget?: string;
310325
}
311326

312327
interface IPlatformDataComposition {

lib/definitions/project.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,12 @@ interface IPlatformProjectService extends NodeJS.EventEmitter, IPlatformProjectS
449449
* Traverse through the production dependencies and find plugins that need build/rebuild
450450
*/
451451
checkIfPluginsNeedBuild(projectData: IProjectData): Promise<Array<any>>;
452+
453+
/**
454+
* Get the deployment target's version
455+
* Currently implemented only for iOS -> returns the value of IPHONEOS_DEPLOYMENT_TARGET property from xcconfig file
456+
*/
457+
getDeploymentTarget(projectData: IProjectData): any;
452458
}
453459

454460
interface IValidatePlatformOutput {

lib/services/android-project-service.ts

+2
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
665665
// Nothing android specific to check yet.
666666
}
667667

668+
public getDeploymentTarget(projectData: IProjectData): semver.SemVer { return; }
669+
668670
private copy(projectRoot: string, frameworkDir: string, files: string, cpArg: string): void {
669671
const paths = files.split(' ').map(p => path.join(frameworkDir, p));
670672
shell.cp(cpArg, paths, projectRoot);

lib/services/ios-project-service.ts

+36-13
Original file line numberDiff line numberDiff line change
@@ -384,12 +384,6 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
384384
}
385385

386386
private async buildForDevice(projectRoot: string, args: string[], buildConfig: IBuildConfig, projectData: IProjectData): Promise<void> {
387-
const defaultArchitectures = [
388-
'ARCHS=armv7 arm64',
389-
'VALID_ARCHS=armv7 arm64'
390-
];
391-
392-
// build only for device specific architecture
393387
if (!buildConfig.release && !buildConfig.architectures) {
394388
await this.$devicesService.initialize({
395389
platform: this.$devicePlatformsConstants.iOS.toLowerCase(), deviceId: buildConfig.device,
@@ -402,18 +396,15 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
402396
.uniq()
403397
.value();
404398
if (devicesArchitectures.length > 0) {
405-
const architectures = [
406-
`ARCHS=${devicesArchitectures.join(" ")}`,
407-
`VALID_ARCHS=${devicesArchitectures.join(" ")}`
408-
];
399+
const architectures = this.getBuildArchitectures(projectData, buildConfig, devicesArchitectures);
409400
if (devicesArchitectures.length > 1) {
410401
architectures.push('ONLY_ACTIVE_ARCH=NO');
411402
}
412403
buildConfig.architectures = architectures;
413404
}
414405
}
415406

416-
args = args.concat((buildConfig && buildConfig.architectures) || defaultArchitectures);
407+
args = args.concat((buildConfig && buildConfig.architectures) || this.getBuildArchitectures(projectData, buildConfig, ["armv7", "arm64"]));
417408

418409
args = args.concat([
419410
"-sdk", "iphoneos",
@@ -457,6 +448,28 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
457448
return commandResult;
458449
}
459450

451+
private getBuildArchitectures(projectData: IProjectData, buildConfig: IBuildConfig, architectures: string[]): string[] {
452+
let result: string[] = [];
453+
454+
const frameworkVersion = this.getFrameworkVersion(projectData);
455+
if (semver.valid(frameworkVersion) && semver.validRange(frameworkVersion) && semver.lt(semver.coerce(frameworkVersion), "5.1.0")) {
456+
const target = this.getDeploymentTarget(projectData);
457+
if (target && target.major >= 11) {
458+
// We need to strip 32bit architectures as of deployment target >= 11 it is not allowed to have such
459+
architectures = _.filter(architectures, arch => {
460+
const is64BitArchitecture = arch === "x86_64" || arch === "arm64";
461+
if (!is64BitArchitecture) {
462+
this.$logger.warn(`The architecture ${arch} will be stripped as it is not supported for deployment target ${target.version}.`);
463+
}
464+
return is64BitArchitecture;
465+
});
466+
}
467+
result = [`ARCHS=${architectures.join(" ")}`, `VALID_ARCHS=${architectures.join(" ")}`];
468+
}
469+
470+
return result;
471+
}
472+
460473
private async setupSigningFromTeam(projectRoot: string, projectData: IProjectData, teamId: string) {
461474
const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData));
462475
const signing = xcode.getSigning(projectData.projectName);
@@ -555,13 +568,14 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
555568
}
556569

557570
private async buildForSimulator(projectRoot: string, args: string[], projectData: IProjectData, buildConfig?: IBuildConfig): Promise<void> {
571+
const architectures = this.getBuildArchitectures(projectData, buildConfig, ["i386", "x86_64"]);
572+
558573
args = args
574+
.concat(architectures)
559575
.concat([
560576
"build",
561577
"-configuration", buildConfig.release ? "Release" : "Debug",
562578
"-sdk", "iphonesimulator",
563-
"ARCHS=i386 x86_64",
564-
"VALID_ARCHS=i386 x86_64",
565579
"ONLY_ACTIVE_ARCH=NO",
566580
"CONFIGURATION_BUILD_DIR=" + path.join(projectRoot, "build", "emulator"),
567581
"CODE_SIGN_IDENTITY=",
@@ -1031,6 +1045,15 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f
10311045
return [];
10321046
}
10331047

1048+
public getDeploymentTarget(projectData: IProjectData): semver.SemVer {
1049+
const target = this.$xCConfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "IPHONEOS_DEPLOYMENT_TARGET");
1050+
if (!target) {
1051+
return null;
1052+
}
1053+
1054+
return semver.coerce(target);
1055+
}
1056+
10341057
private getAllLibsForPluginWithFileExtension(pluginData: IPluginData, fileExtension: string): string[] {
10351058
const filterCallback = (fileName: string, pluginPlatformsFolderPath: string) => path.extname(fileName) === fileExtension;
10361059
return this.getAllNativeLibrariesForPlugin(pluginData, IOSProjectService.IOS_PLATFORM_NAME, filterCallback);

lib/services/livesync/livesync-service.ts

+1
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
470470
});
471471
}
472472

473+
await this.$platformService.validateInstall(options.device, options.projectData, options, options.deviceBuildInfoDescriptor.outputPath);
473474
const shouldInstall = await this.$platformService.shouldInstall(options.device, options.projectData, options, options.deviceBuildInfoDescriptor.outputPath);
474475
if (shouldInstall) {
475476
await this.$platformService.installApplication(options.device, { release: false }, options.projectData, pathToBuildItem, options.deviceBuildInfoDescriptor.outputPath);

lib/services/platform-service.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -436,13 +436,20 @@ export class PlatformService extends EventEmitter implements IPlatformService {
436436

437437
public saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void {
438438
const buildInfoFile = path.join(buildInfoFileDirname, buildInfoFileName);
439+
const projectData = this.$projectDataService.getProjectData(projectDir);
440+
const platformData = this.$platformsData.getPlatformData(platform, projectData);
439441

440-
const prepareInfo = this.$projectChangesService.getPrepareInfo(platform, this.$projectDataService.getProjectData(projectDir));
441-
const buildInfo = {
442+
const prepareInfo = this.$projectChangesService.getPrepareInfo(platform, projectData);
443+
const buildInfo: IBuildInfo = {
442444
prepareTime: prepareInfo.changesRequireBuildTime,
443445
buildTime: new Date().toString()
444446
};
445447

448+
const deploymentTarget = platformData.platformProjectService.getDeploymentTarget(projectData);
449+
if (deploymentTarget) {
450+
buildInfo.deploymentTarget = deploymentTarget.version;
451+
}
452+
446453
this.$fs.writeJson(buildInfoFile, buildInfo);
447454
}
448455

@@ -455,9 +462,21 @@ export class PlatformService extends EventEmitter implements IPlatformService {
455462
const platformData = this.$platformsData.getPlatformData(platform, projectData);
456463
const deviceBuildInfo: IBuildInfo = await this.getDeviceBuildInfo(device, projectData);
457464
const localBuildInfo = this.getBuildInfo(platform, platformData, { buildForDevice: !device.isEmulator, release: release.release }, outputPath);
465+
458466
return !localBuildInfo || !deviceBuildInfo || deviceBuildInfo.buildTime !== localBuildInfo.buildTime;
459467
}
460468

469+
public async validateInstall(device: Mobile.IDevice, projectData: IProjectData, release: IRelease, outputPath?: string): Promise<void> {
470+
const platform = device.deviceInfo.platform;
471+
const platformData = this.$platformsData.getPlatformData(platform, projectData);
472+
const localBuildInfo = this.getBuildInfo(device.deviceInfo.platform, platformData, { buildForDevice: !device.isEmulator, release: release.release }, outputPath);
473+
if (localBuildInfo.deploymentTarget) {
474+
if (semver.lt(semver.coerce(device.deviceInfo.version), semver.coerce(localBuildInfo.deploymentTarget))) {
475+
this.$errors.fail(`Unable to install on device with version ${device.deviceInfo.version} as deployment target is ${localBuildInfo.deploymentTarget}`);
476+
}
477+
}
478+
}
479+
461480
public async installApplication(device: Mobile.IDevice, buildConfig: IBuildConfig, projectData: IProjectData, packageFile?: string, outputFilePath?: string): Promise<void> {
462481
this.$logger.out(`Installing on device ${device.deviceInfo.identifier}...`);
463482

test/ios-project-service.ts

+155
Original file line numberDiff line numberDiff line change
@@ -1055,3 +1055,158 @@ describe("Merge Project XCConfig files", () => {
10551055
}
10561056
});
10571057
});
1058+
1059+
describe("buildProject", () => {
1060+
let xcodeBuildCommandArgs: string[] = [];
1061+
1062+
function setup(data: { frameworkVersion: string, deploymentTarget: string, devices?: Mobile.IDevice[] }): IInjector {
1063+
const projectPath = "myTestProjectPath";
1064+
const projectName = "myTestProjectName";
1065+
const testInjector = createTestInjector(projectPath, projectName);
1066+
1067+
const childProcess = testInjector.resolve("childProcess");
1068+
childProcess.spawnFromEvent = (command: string, args: string[]) => {
1069+
if (command === "xcodebuild" && args[0] !== "-exportArchive") {
1070+
xcodeBuildCommandArgs = args;
1071+
}
1072+
};
1073+
1074+
const projectDataService = testInjector.resolve("projectDataService");
1075+
projectDataService.getNSValue = (projectDir: string, propertyName: string) => {
1076+
if (propertyName === "tns-ios") {
1077+
return {
1078+
name: "tns-ios",
1079+
version: data.frameworkVersion
1080+
};
1081+
}
1082+
};
1083+
1084+
const projectData = testInjector.resolve("projectData");
1085+
projectData.appResourcesDirectoryPath = path.join(projectPath, "app", "App_Resources");
1086+
1087+
const devicesService = testInjector.resolve("devicesService");
1088+
devicesService.initialize = () => ({});
1089+
devicesService.getDeviceInstances = () => data.devices || [];
1090+
1091+
const xCConfigService = testInjector.resolve("xCConfigService");
1092+
xCConfigService.readPropertyValue = (projectDir: string, propertyName: string) => {
1093+
if (propertyName === "IPHONEOS_DEPLOYMENT_TARGET") {
1094+
return data.deploymentTarget;
1095+
}
1096+
};
1097+
1098+
const pbxprojDomXcode = testInjector.resolve("pbxprojDomXcode");
1099+
pbxprojDomXcode.Xcode = {
1100+
open: () => ({
1101+
getSigning: () => ({}),
1102+
setAutomaticSigningStyle: () => ({}),
1103+
save: () => ({})
1104+
})
1105+
};
1106+
1107+
const iOSProvisionService = testInjector.resolve("iOSProvisionService");
1108+
iOSProvisionService.getDevelopmentTeams = () => ({});
1109+
iOSProvisionService.getTeamIdsWithName = () => ({});
1110+
1111+
return testInjector;
1112+
}
1113+
1114+
function executeTests(testCases: any[], data: { buildForDevice: boolean }) {
1115+
_.each(testCases, testCase => {
1116+
it(`${testCase.name}`, async () => {
1117+
const testInjector = setup({ frameworkVersion: testCase.frameworkVersion, deploymentTarget: testCase.deploymentTarget });
1118+
const projectData: IProjectData = testInjector.resolve("projectData");
1119+
1120+
const iOSProjectService = <IOSProjectService>testInjector.resolve("iOSProjectService");
1121+
(<any>iOSProjectService).getExportOptionsMethod = () => ({});
1122+
await iOSProjectService.buildProject("myProjectRoot", projectData, <any>{ buildForDevice: data.buildForDevice });
1123+
1124+
const archsItem = xcodeBuildCommandArgs.find(item => item.startsWith("ARCHS="));
1125+
if (testCase.expectedArchs) {
1126+
const archsValue = archsItem.split("=")[1];
1127+
assert.deepEqual(archsValue, testCase.expectedArchs);
1128+
} else {
1129+
assert.deepEqual(undefined, archsItem);
1130+
}
1131+
});
1132+
});
1133+
}
1134+
1135+
describe("for device", () => {
1136+
afterEach(() => {
1137+
xcodeBuildCommandArgs = [];
1138+
});
1139+
1140+
const testCases = <any[]>[{
1141+
name: "shouldn't exclude armv7 architecture when deployment target 10",
1142+
frameworkVersion: "5.0.0",
1143+
deploymentTarget: "10.0",
1144+
expectedArchs: "armv7 arm64"
1145+
}, {
1146+
name: "should exclude armv7 architecture when deployment target is 11",
1147+
frameworkVersion: "5.0.0",
1148+
deploymentTarget: "11.0",
1149+
expectedArchs: "arm64"
1150+
}, {
1151+
name: "shouldn't pass architecture to xcodebuild command when frameworkVersion is 5.1.0",
1152+
frameworkVersion: "5.1.0",
1153+
deploymentTarget: "11.0"
1154+
}, {
1155+
name: "should pass only 64bit architecture to xcodebuild command when frameworkVersion is 5.0.0 and deployment target is 11.0",
1156+
frameworkVersion: "5.0.0",
1157+
deploymentTarget: "11.0",
1158+
expectedArchs: "arm64"
1159+
}, {
1160+
name: "should pass both architectures to xcodebuild command when frameworkVersion is 5.0.0 and deployment target is 10.0",
1161+
frameworkVersion: "5.0.0",
1162+
deploymentTarget: "10.0",
1163+
expectedArchs: "armv7 arm64"
1164+
}, {
1165+
name: "should pass both architectures to xcodebuild command when frameworkVersion is 5.0.0 and no deployment target",
1166+
frameworkVersion: "5.0.0",
1167+
deploymentTarget: null,
1168+
expectedArchs: "armv7 arm64"
1169+
}];
1170+
1171+
executeTests(testCases, { buildForDevice: true });
1172+
});
1173+
1174+
describe("for simulator", () => {
1175+
afterEach(() => {
1176+
xcodeBuildCommandArgs = [];
1177+
});
1178+
1179+
const testCases = [{
1180+
name: "shouldn't exclude i386 architecture when deployment target is 10",
1181+
frameworkVersion: "5.0.0",
1182+
deploymentTarget: "10.0",
1183+
expectedArchs: "i386 x86_64"
1184+
}, {
1185+
name: "should exclude i386 architecture when deployment target is 11",
1186+
frameworkVersion: "5.0.0",
1187+
deploymentTarget: "11.0",
1188+
expectedArchs: "x86_64"
1189+
}, {
1190+
name: "shouldn't pass architecture to xcodebuild command when frameworkVersion is 5.1.0",
1191+
frameworkVersion: "5.1.0",
1192+
deploymentTarget: "11.0"
1193+
}, {
1194+
name: "should pass only 64bit architecture to xcodebuild command when frameworkVersion is 5.0.0 and deployment target is 11.0",
1195+
frameworkVersion: "5.0.0",
1196+
deploymentTarget: "11.0",
1197+
expectedArchs: "x86_64"
1198+
}, {
1199+
name: "should pass both architectures to xcodebuild command when frameworkVersion is 5.0.0 and deployment target is 10.0",
1200+
frameworkVersion: "5.0.0",
1201+
deploymentTarget: "10.0",
1202+
expectedArchs: "i386 x86_64"
1203+
}, {
1204+
name: "should pass both architectures to xcodebuild command when frameworkVersion is 5.0.0 and no deployment target",
1205+
frameworkVersion: "5.0.0",
1206+
deploymentTarget: null,
1207+
expectedArchs: "i386 x86_64"
1208+
}];
1209+
1210+
executeTests(testCases, { buildForDevice: false });
1211+
});
1212+
});

test/stubs.ts

+7
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,9 @@ export class PlatformProjectServiceStub extends EventEmitter implements IPlatfor
468468
getPluginPlatformsFolderPath(pluginData: IPluginData, platform: string): string {
469469
return "";
470470
}
471+
getDeploymentTarget(projectData: IProjectData): any {
472+
return;
473+
}
471474
}
472475

473476
export class PlatformsDataStub extends EventEmitter implements IPlatformsData {
@@ -836,6 +839,10 @@ export class PlatformServiceStub extends EventEmitter implements IPlatformServic
836839
return true;
837840
}
838841

842+
public async validateInstall(device: Mobile.IDevice): Promise<void> {
843+
return;
844+
}
845+
839846
public installApplication(device: Mobile.IDevice, options: IRelease): Promise<void> {
840847
return Promise.resolve();
841848
}

0 commit comments

Comments
 (0)