Skip to content

Commit 6cf36d8

Browse files
DmitriyKirakosyanlucen-ms
authored andcommitted
[CLI] Improve binary app version check for iOS
In addition to checking Info.plist, also check Xcode project file for MARKETING_VERSION
1 parent 00322ee commit 6cf36d8

File tree

8 files changed

+189
-20
lines changed

8 files changed

+189
-20
lines changed

cli/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,9 @@ code-push-standalone release-react <appName> <platform>
396396
[--podFile <podFile>]
397397
[--extraHermesFlags <extraHermesFlags>]
398398
[--privateKeyPath <privateKeyPath>]
399+
[--xcodeProjectFile <xcodeProjectFile>]
400+
[--xcodeTargetName <xcodeTargetName>]
401+
[--buildConfigurationName <buildConfigurationName>]
399402
```
400403

401404
The `release-react` command is a React Native-specific version of the "vanilla" [`release`](#releasing-app-updates) command, which supports all of the same parameters (e.g. `--mandatory`, `--description`), yet simplifies the process of releasing updates by performing the following additional behavior:
@@ -549,6 +552,24 @@ Private key path which is used for code signing.
549552

550553
_NOTE: This parameter can be set using either --privateKeyPath or -k_
551554

555+
#### Xcode project file parameter
556+
557+
Path to the Xcode project or project.pbxproj file.
558+
559+
_NOTE: This parameter can be set using either --xcodeProjectFile or -xp_
560+
561+
#### Xcode target name parameter
562+
563+
Name of target (PBXNativeTarget) which specifies the binary version you want to target this release at (iOS only).
564+
565+
_NOTE: This parameter can be set using either --xcodeTargetName or -xt_
566+
567+
#### Build configuration name parameter
568+
569+
Name of build configuration which specifies the binary version you want to target this release at. For example, 'Debug' or 'Release' (iOS only).
570+
571+
_NOTE: This parameter can be set using either --buildConfigurationName or -c_
572+
552573
## Debugging CodePush Integration
553574

554575
Once you've released an update, React Native plugin has been integrated into your app, it can be helpful to diagnose how the plugin is behaving, especially if you run into an issue and want to understand why. In order to debug the CodePush update discovery experience, you can run the following command in order to easily view the diagnostic logs produced by the CodePush plugin within your app:

cli/package-lock.json

Lines changed: 66 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"temp": "^0.9.4",
4343
"which": "^1.2.7",
4444
"wordwrap": "1.0.0",
45+
"xcode": "^3.0.1",
4546
"xml2js": "^0.6.0",
4647
"yargs": "^17.7.2",
4748
"yazl": "^2.5.1"

cli/script/command-executor.ts

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const which = require("which");
2222
import wordwrap = require("wordwrap");
2323
import * as cli from "../script/types/cli";
2424
import sign from "./sign";
25+
const xcode = require("xcode");
2526
import {
2627
AccessKey,
2728
Account,
@@ -40,11 +41,13 @@ import {
4041
import {
4142
getAndroidHermesEnabled,
4243
getiOSHermesEnabled,
43-
runHermesEmitBinaryCommand
44+
runHermesEmitBinaryCommand,
45+
isValidVersion
4446
} from "./react-native-utils";
4547
import {
4648
fileDoesNotExistOrIsDirectory,
47-
isBinaryOrZip
49+
isBinaryOrZip,
50+
fileExists
4851
} from "./utils/file-utils";
4952

5053
const configFilePath: string = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".code-push.config");
@@ -855,16 +858,6 @@ function getPackageMetricsString(obj: Package): string {
855858
}
856859

857860
function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, projectName: string): Promise<string> {
858-
const fileExists = (file: string): boolean => {
859-
try {
860-
return fs.statSync(file).isFile();
861-
} catch (e) {
862-
return false;
863-
}
864-
};
865-
866-
const isValidVersion = (version: string): boolean => !!semver.valid(version) || /^\d+\.\d+$/.test(version);
867-
868861
log(chalk.cyan(`Detecting ${command.platform} app version:\n`));
869862

870863
if (command.platform === "ios") {
@@ -914,9 +907,13 @@ function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, proj
914907
log(`Using the target binary version value "${parsedPlist.CFBundleShortVersionString}" from "${resolvedPlistFile}".\n`);
915908
return Q(parsedPlist.CFBundleShortVersionString);
916909
} else {
917-
throw new Error(
918-
`The "CFBundleShortVersionString" key in the "${resolvedPlistFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`
919-
);
910+
if (parsedPlist.CFBundleShortVersionString !== "$(MARKETING_VERSION)") {
911+
throw new Error(
912+
`The "CFBundleShortVersionString" key in the "${resolvedPlistFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`
913+
);
914+
}
915+
916+
return getAppVersionFromXcodeProject(command, projectName);
920917
}
921918
} else {
922919
throw new Error(`The "CFBundleShortVersionString" key doesn't exist within the "${resolvedPlistFile}" file.`);
@@ -1052,6 +1049,53 @@ function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, proj
10521049
}
10531050
}
10541051

1052+
function getAppVersionFromXcodeProject(command: cli.IReleaseReactCommand, projectName: string): Promise<string> {
1053+
const pbxprojFileName = "project.pbxproj";
1054+
let resolvedPbxprojFile: string = command.xcodeProjectFile;
1055+
if (resolvedPbxprojFile) {
1056+
// If the xcode project file path is explicitly provided, then we don't
1057+
// need to attempt to "resolve" it within the well-known locations.
1058+
if (!resolvedPbxprojFile.endsWith(pbxprojFileName)) {
1059+
// Specify path to pbxproj file if the provided file path is an Xcode project file.
1060+
resolvedPbxprojFile = path.join(resolvedPbxprojFile, pbxprojFileName);
1061+
}
1062+
if (!fileExists(resolvedPbxprojFile)) {
1063+
throw new Error("The specified pbx project file doesn't exist. Please check that the provided path is correct.");
1064+
}
1065+
} else {
1066+
const iOSDirectory = "ios";
1067+
const xcodeprojDirectory = `${projectName}.xcodeproj`;
1068+
const pbxprojKnownLocations = [
1069+
path.join(iOSDirectory, xcodeprojDirectory, pbxprojFileName),
1070+
path.join(iOSDirectory, pbxprojFileName),
1071+
];
1072+
resolvedPbxprojFile = pbxprojKnownLocations.find(fileExists);
1073+
1074+
if (!resolvedPbxprojFile) {
1075+
throw new Error(
1076+
`Unable to find either of the following pbxproj files in order to infer your app's binary version: "${pbxprojKnownLocations.join(
1077+
'", "'
1078+
)}".`
1079+
);
1080+
}
1081+
}
1082+
1083+
const xcodeProj = xcode.project(resolvedPbxprojFile).parseSync();
1084+
const marketingVersion = xcodeProj.getBuildProperty(
1085+
"MARKETING_VERSION",
1086+
command.buildConfigurationName,
1087+
command.xcodeTargetName
1088+
);
1089+
if (!isValidVersion(marketingVersion)) {
1090+
throw new Error(
1091+
`The "MARKETING_VERSION" key in the "${resolvedPbxprojFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).`
1092+
);
1093+
}
1094+
console.log(`Using the target binary version value "${marketingVersion}" from "${resolvedPbxprojFile}".\n`);
1095+
1096+
return marketingVersion;
1097+
}
1098+
10551099
function printJson(object: any): void {
10561100
log(JSON.stringify(object, /*replacer=*/ null, /*spacing=*/ 2));
10571101
}
@@ -1278,10 +1322,6 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
12781322
}
12791323
}
12801324

1281-
if (command.appStoreVersion) {
1282-
throwForInvalidSemverRange(command.appStoreVersion);
1283-
}
1284-
12851325
const appVersionPromise: Promise<string> = command.appStoreVersion
12861326
? Q(command.appStoreVersion)
12871327
: getReactNativeProjectAppVersion(command, projectName);
@@ -1293,7 +1333,9 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
12931333
return appVersionPromise;
12941334
})
12951335
.then((appVersion: string) => {
1336+
throwForInvalidSemverRange(appVersion);
12961337
releaseCommand.appStoreVersion = appVersion;
1338+
12971339
return createEmptyTempReleaseFolder(outputFolder);
12981340
})
12991341
// This is needed to clear the react native bundler cache:

cli/script/command-parser.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,27 @@ yargs
804804
description: "Path to private key used for code signing.",
805805
type: "string",
806806
})
807+
.option("xcodeProjectFile", {
808+
alias: "xp",
809+
default: null,
810+
demand: false,
811+
description: "Path to the Xcode project or project.pbxproj file",
812+
type: "string",
813+
})
814+
.option("xcodeTargetName", {
815+
alias: "xt",
816+
default: undefined,
817+
demand: false,
818+
description: "Name of target (PBXNativeTarget) which specifies the binary version you want to target this release at (iOS only)",
819+
type: "string",
820+
})
821+
.option("buildConfigurationName", {
822+
alias: "c",
823+
default: undefined,
824+
demand: false,
825+
description: "Name of build configuration which specifies the binary version you want to target this release at. For example, 'Debug' or 'Release' (iOS only)",
826+
type: "string",
827+
})
807828
.check((argv: any, aliases: { [aliases: string]: string }): any => {
808829
return checkValidReleaseOptions(argv);
809830
});
@@ -1202,6 +1223,9 @@ export function createCommand(): cli.ICommand {
12021223
releaseReactCommand.extraHermesFlags = argv["extraHermesFlags"] as any;
12031224
releaseReactCommand.podFile = argv["podFile"] as any;
12041225
releaseReactCommand.privateKeyPath = argv["privateKeyPath"] as any;
1226+
releaseReactCommand.xcodeProjectFile = argv["xcodeProjectFile"] as any;
1227+
releaseReactCommand.xcodeTargetName = argv["xcodeTargetName"] as any;
1228+
releaseReactCommand.buildConfigurationName = argv["buildConfigurationName"] as any;
12051229
}
12061230
break;
12071231

cli/script/react-native-utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ import * as fs from "fs";
22
import * as chalk from "chalk";
33
import * as path from "path";
44
import * as childProcess from "child_process";
5-
import { coerce, compare } from "semver";
5+
import { coerce, compare, valid } from "semver";
66
import { fileDoesNotExistOrIsDirectory } from "./utils/file-utils";
77

88
const g2js = require("gradle-to-js/lib/parser");
99

10+
export function isValidVersion(version: string): boolean {
11+
return !!valid(version) || /^\d+\.\d+$/.test(version);
12+
}
13+
1014
export async function runHermesEmitBinaryCommand(
1115
bundleName: string,
1216
outputFolder: string,

cli/script/types/cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ export interface IReleaseReactCommand extends IReleaseBaseCommand {
201201
useHermes?: boolean;
202202
extraHermesFlags?: string[];
203203
podFile?: string;
204+
xcodeProjectFile?: string;
205+
xcodeTargetName?: string;
206+
buildConfigurationName?: string;
204207
}
205208

206209
export interface IRollbackCommand extends ICommand {

cli/script/utils/file-utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ export function isDirectory(path: string): boolean {
1111
return fs.statSync(path).isDirectory();
1212
}
1313

14+
export function fileExists(file: string): boolean {
15+
try {
16+
return fs.statSync(file).isFile();
17+
} catch (e) {
18+
return false;
19+
}
20+
};
21+
1422
export function copyFileToTmpDir(filePath: string): string {
1523
if (!isDirectory(filePath)) {
1624
const outputFolderPath: string = temp.mkdirSync("code-push");

0 commit comments

Comments
 (0)