Skip to content

Commit

Permalink
[CLI] Improve binary app version check for iOS
Browse files Browse the repository at this point in the history
In addition to checking Info.plist, also check Xcode project file for
MARKETING_VERSION
  • Loading branch information
DmitriyKirakosyan authored and lucen-ms committed Nov 6, 2024
1 parent 00322ee commit 6cf36d8
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 20 deletions.
21 changes: 21 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,9 @@ code-push-standalone release-react <appName> <platform>
[--podFile <podFile>]
[--extraHermesFlags <extraHermesFlags>]
[--privateKeyPath <privateKeyPath>]
[--xcodeProjectFile <xcodeProjectFile>]
[--xcodeTargetName <xcodeTargetName>]
[--buildConfigurationName <buildConfigurationName>]
```

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:
Expand Down Expand Up @@ -549,6 +552,24 @@ Private key path which is used for code signing.

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

#### Xcode project file parameter

Path to the Xcode project or project.pbxproj file.

_NOTE: This parameter can be set using either --xcodeProjectFile or -xp_

#### Xcode target name parameter

Name of target (PBXNativeTarget) which specifies the binary version you want to target this release at (iOS only).

_NOTE: This parameter can be set using either --xcodeTargetName or -xt_

#### Build configuration name parameter

Name of build configuration which specifies the binary version you want to target this release at. For example, 'Debug' or 'Release' (iOS only).

_NOTE: This parameter can be set using either --buildConfigurationName or -c_

## Debugging CodePush Integration

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:
Expand Down
66 changes: 66 additions & 0 deletions cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"temp": "^0.9.4",
"which": "^1.2.7",
"wordwrap": "1.0.0",
"xcode": "^3.0.1",
"xml2js": "^0.6.0",
"yargs": "^17.7.2",
"yazl": "^2.5.1"
Expand Down
80 changes: 61 additions & 19 deletions cli/script/command-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const which = require("which");
import wordwrap = require("wordwrap");
import * as cli from "../script/types/cli";
import sign from "./sign";
const xcode = require("xcode");
import {
AccessKey,
Account,
Expand All @@ -40,11 +41,13 @@ import {
import {
getAndroidHermesEnabled,
getiOSHermesEnabled,
runHermesEmitBinaryCommand
runHermesEmitBinaryCommand,
isValidVersion
} from "./react-native-utils";
import {
fileDoesNotExistOrIsDirectory,
isBinaryOrZip
isBinaryOrZip,
fileExists
} from "./utils/file-utils";

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

function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, projectName: string): Promise<string> {
const fileExists = (file: string): boolean => {
try {
return fs.statSync(file).isFile();
} catch (e) {
return false;
}
};

const isValidVersion = (version: string): boolean => !!semver.valid(version) || /^\d+\.\d+$/.test(version);

log(chalk.cyan(`Detecting ${command.platform} app version:\n`));

if (command.platform === "ios") {
Expand Down Expand Up @@ -914,9 +907,13 @@ function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, proj
log(`Using the target binary version value "${parsedPlist.CFBundleShortVersionString}" from "${resolvedPlistFile}".\n`);
return Q(parsedPlist.CFBundleShortVersionString);
} else {
throw new Error(
`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).`
);
if (parsedPlist.CFBundleShortVersionString !== "$(MARKETING_VERSION)") {
throw new Error(
`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).`
);
}

return getAppVersionFromXcodeProject(command, projectName);
}
} else {
throw new Error(`The "CFBundleShortVersionString" key doesn't exist within the "${resolvedPlistFile}" file.`);
Expand Down Expand Up @@ -1052,6 +1049,53 @@ function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, proj
}
}

function getAppVersionFromXcodeProject(command: cli.IReleaseReactCommand, projectName: string): Promise<string> {
const pbxprojFileName = "project.pbxproj";
let resolvedPbxprojFile: string = command.xcodeProjectFile;
if (resolvedPbxprojFile) {
// If the xcode project file path is explicitly provided, then we don't
// need to attempt to "resolve" it within the well-known locations.
if (!resolvedPbxprojFile.endsWith(pbxprojFileName)) {
// Specify path to pbxproj file if the provided file path is an Xcode project file.
resolvedPbxprojFile = path.join(resolvedPbxprojFile, pbxprojFileName);
}
if (!fileExists(resolvedPbxprojFile)) {
throw new Error("The specified pbx project file doesn't exist. Please check that the provided path is correct.");
}
} else {
const iOSDirectory = "ios";
const xcodeprojDirectory = `${projectName}.xcodeproj`;
const pbxprojKnownLocations = [
path.join(iOSDirectory, xcodeprojDirectory, pbxprojFileName),
path.join(iOSDirectory, pbxprojFileName),
];
resolvedPbxprojFile = pbxprojKnownLocations.find(fileExists);

if (!resolvedPbxprojFile) {
throw new Error(
`Unable to find either of the following pbxproj files in order to infer your app's binary version: "${pbxprojKnownLocations.join(
'", "'
)}".`
);
}
}

const xcodeProj = xcode.project(resolvedPbxprojFile).parseSync();
const marketingVersion = xcodeProj.getBuildProperty(
"MARKETING_VERSION",
command.buildConfigurationName,
command.xcodeTargetName
);
if (!isValidVersion(marketingVersion)) {
throw new Error(
`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).`
);
}
console.log(`Using the target binary version value "${marketingVersion}" from "${resolvedPbxprojFile}".\n`);

return marketingVersion;
}

function printJson(object: any): void {
log(JSON.stringify(object, /*replacer=*/ null, /*spacing=*/ 2));
}
Expand Down Expand Up @@ -1278,10 +1322,6 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
}
}

if (command.appStoreVersion) {
throwForInvalidSemverRange(command.appStoreVersion);
}

const appVersionPromise: Promise<string> = command.appStoreVersion
? Q(command.appStoreVersion)
: getReactNativeProjectAppVersion(command, projectName);
Expand All @@ -1293,7 +1333,9 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
return appVersionPromise;
})
.then((appVersion: string) => {
throwForInvalidSemverRange(appVersion);
releaseCommand.appStoreVersion = appVersion;

return createEmptyTempReleaseFolder(outputFolder);
})
// This is needed to clear the react native bundler cache:
Expand Down
24 changes: 24 additions & 0 deletions cli/script/command-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,27 @@ yargs
description: "Path to private key used for code signing.",
type: "string",
})
.option("xcodeProjectFile", {
alias: "xp",
default: null,
demand: false,
description: "Path to the Xcode project or project.pbxproj file",
type: "string",
})
.option("xcodeTargetName", {
alias: "xt",
default: undefined,
demand: false,
description: "Name of target (PBXNativeTarget) which specifies the binary version you want to target this release at (iOS only)",
type: "string",
})
.option("buildConfigurationName", {
alias: "c",
default: undefined,
demand: false,
description: "Name of build configuration which specifies the binary version you want to target this release at. For example, 'Debug' or 'Release' (iOS only)",
type: "string",
})
.check((argv: any, aliases: { [aliases: string]: string }): any => {
return checkValidReleaseOptions(argv);
});
Expand Down Expand Up @@ -1202,6 +1223,9 @@ export function createCommand(): cli.ICommand {
releaseReactCommand.extraHermesFlags = argv["extraHermesFlags"] as any;
releaseReactCommand.podFile = argv["podFile"] as any;
releaseReactCommand.privateKeyPath = argv["privateKeyPath"] as any;
releaseReactCommand.xcodeProjectFile = argv["xcodeProjectFile"] as any;
releaseReactCommand.xcodeTargetName = argv["xcodeTargetName"] as any;
releaseReactCommand.buildConfigurationName = argv["buildConfigurationName"] as any;
}
break;

Expand Down
6 changes: 5 additions & 1 deletion cli/script/react-native-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import * as fs from "fs";
import * as chalk from "chalk";
import * as path from "path";
import * as childProcess from "child_process";
import { coerce, compare } from "semver";
import { coerce, compare, valid } from "semver";
import { fileDoesNotExistOrIsDirectory } from "./utils/file-utils";

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

export function isValidVersion(version: string): boolean {
return !!valid(version) || /^\d+\.\d+$/.test(version);
}

export async function runHermesEmitBinaryCommand(
bundleName: string,
outputFolder: string,
Expand Down
3 changes: 3 additions & 0 deletions cli/script/types/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ export interface IReleaseReactCommand extends IReleaseBaseCommand {
useHermes?: boolean;
extraHermesFlags?: string[];
podFile?: string;
xcodeProjectFile?: string;
xcodeTargetName?: string;
buildConfigurationName?: string;
}

export interface IRollbackCommand extends ICommand {
Expand Down
8 changes: 8 additions & 0 deletions cli/script/utils/file-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export function isDirectory(path: string): boolean {
return fs.statSync(path).isDirectory();
}

export function fileExists(file: string): boolean {
try {
return fs.statSync(file).isFile();
} catch (e) {
return false;
}
};

export function copyFileToTmpDir(filePath: string): string {
if (!isDirectory(filePath)) {
const outputFolderPath: string = temp.mkdirSync("code-push");
Expand Down

0 comments on commit 6cf36d8

Please sign in to comment.