Skip to content

Commit

Permalink
feat(tools-android): add primitives for build and run commands (#3322)
Browse files Browse the repository at this point in the history
  • Loading branch information
tido64 authored Aug 30, 2024
1 parent 923c91f commit c2023a7
Show file tree
Hide file tree
Showing 21 changed files with 565 additions and 319 deletions.
7 changes: 7 additions & 0 deletions .changeset/breezy-mirrors-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rnx-kit/cli": patch
---

Added Android support to the experimental commands for building and running
apps. Again, this still needs more testing and will not be publicly available
yet.
5 changes: 5 additions & 0 deletions .changeset/thin-eyes-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rnx-kit/tools-android": patch
---

Added primitives for building 'build' and 'run' commands
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@rnx-kit/metro-serializer-esbuild": "^0.1.38",
"@rnx-kit/metro-service": "^3.1.6",
"@rnx-kit/third-party-notices": "^1.3.4",
"@rnx-kit/tools-android": "^0.1.0",
"@rnx-kit/tools-apple": "^0.1.2",
"@rnx-kit/tools-language": "^2.0.0",
"@rnx-kit/tools-node": "^2.1.1",
Expand Down
16 changes: 11 additions & 5 deletions packages/cli/src/build.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Config } from "@react-native-community/cli-types";
import { InvalidArgumentError } from "commander";
import { buildAndroid } from "./build/android";
import { buildIOS } from "./build/ios";
import { buildMacOS } from "./build/macos";
import type {
BuildConfiguration,
DeviceType,
InputParams,
} from "./build/apple";
import { buildIOS } from "./build/ios";
import { buildMacOS } from "./build/macos";
} from "./build/types";

function asConfiguration(configuration: string): BuildConfiguration {
switch (configuration) {
Expand Down Expand Up @@ -35,13 +36,14 @@ function asDestination(destination: string): DeviceType {

function asSupportedPlatform(platform: string): InputParams["platform"] {
switch (platform) {
case "android":
case "ios":
case "macos":
case "visionos":
return platform;
default:
throw new InvalidArgumentError(
"Supported platforms: 'ios', 'macos', 'visionos'."
"Supported platforms: 'android', 'ios', 'macos', 'visionos'."
);
}
}
Expand All @@ -52,9 +54,13 @@ export function rnxBuild(
buildParams: InputParams
) {
switch (buildParams.platform) {
case "android":
return buildAndroid(config, buildParams);

case "ios":
case "visionos":
return buildIOS(config, buildParams);

case "macos":
return buildMacOS(config, buildParams);
}
Expand All @@ -78,7 +84,7 @@ export const rnxBuildCommand = {
},
{
name: "--scheme <string>",
description: "Name of scheme to build",
description: "Name of scheme to build (Apple platforms only)",
},
{
name: "--configuration <string>",
Expand Down
39 changes: 39 additions & 0 deletions packages/cli/src/build/android.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Config } from "@react-native-community/cli-types";
import ora from "ora";
import type { AndroidBuildParams } from "./types";

export async function buildAndroid(
config: Config,
buildParams: AndroidBuildParams,
logger = ora()
): Promise<string | number | null> {
const { sourceDir } = config.project.android ?? {};
if (!sourceDir) {
logger.fail("No Android project was found");
process.exitCode = 1;
return null;
}

return import("@rnx-kit/tools-android").then(({ assemble }) => {
return new Promise((resolve) => {
logger.start("Building");

const errors: Buffer[] = [];
const gradle = assemble(sourceDir, buildParams);

gradle.stdout.on("data", () => (logger.text += "."));
gradle.stderr.on("data", (data) => errors.push(data));

gradle.on("close", (code) => {
if (code === 0) {
logger.succeed("Build succeeded");
resolve(sourceDir);
} else {
logger.fail(Buffer.concat(errors).toString());
process.exitCode = code ?? 1;
resolve(code);
}
});
});
});
}
38 changes: 3 additions & 35 deletions packages/cli/src/build/apple.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,5 @@
import type { Ora } from "ora";

// Copy of types from `@rnx-kit/tools-apple`. If Jest wasn't such a pain to
// configure, we would have added an `import type` at the top instead:
//
// import type { BuildParams } from "@rnx-kit/tools-apple" with { "resolution-mode": "import" };
//
// But Jest doesn't like import attributes and it doesn't matter if we add
// `@babel/plugin-syntax-import-attributes` in the config.
//
// TOOD: Remove the `DeviceType`, `BuildConfiguration` and `BuildParams` when we
// can migrate away from Jest in this package.
export type DeviceType = "device" | "emulator" | "simulator";
export type BuildConfiguration = "Debug" | "Release";
type BuildParams =
| {
platform: "ios" | "visionos";
scheme?: string;
destination?: DeviceType;
configuration?: BuildConfiguration;
archs?: string;
isBuiltRemotely?: boolean;
}
| {
platform: "macos";
scheme?: string;
configuration?: BuildConfiguration;
isBuiltRemotely?: boolean;
};
import type { AppleBuildParams } from "./types";

export type BuildArgs = {
xcworkspace: string;
Expand All @@ -35,19 +8,14 @@ export type BuildArgs = {

export type BuildResult = BuildArgs | number | null;

export type InputParams = BuildParams & {
device?: string;
workspace?: string;
};

export function runBuild(
xcworkspace: string,
buildParams: BuildParams,
buildParams: AppleBuildParams,
logger: Ora
): Promise<BuildResult> {
return import("@rnx-kit/tools-apple").then(({ xcodebuild }) => {
return new Promise<BuildResult>((resolve) => {
logger.start("Building...");
logger.start("Building");

const errors: Buffer[] = [];
const proc = xcodebuild(xcworkspace, buildParams, (text) => {
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/build/ios.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Config } from "@react-native-community/cli-types";
import * as path from "node:path";
import ora from "ora";
import type { BuildResult, InputParams } from "./apple";
import type { BuildResult } from "./apple";
import { runBuild } from "./apple";
import type { AppleInputParams } from "./types";

export function buildIOS(
config: Config,
buildParams: InputParams,
buildParams: AppleInputParams,
logger = ora()
): Promise<BuildResult> {
const { platform } = buildParams;
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/build/macos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import type { Config } from "@react-native-community/cli-types";
import * as fs from "node:fs";
import * as path from "node:path";
import ora from "ora";
import type { BuildResult, InputParams } from "./apple";
import type { BuildResult } from "./apple";
import { runBuild } from "./apple";
import type { AppleInputParams } from "./types";

function findXcodeWorkspaces(searchDir: string) {
return fs.existsSync(searchDir)
Expand All @@ -13,7 +14,7 @@ function findXcodeWorkspaces(searchDir: string) {

export function buildMacOS(
_config: Config,
{ workspace, ...buildParams }: InputParams,
{ workspace, ...buildParams }: AppleInputParams,
logger = ora()
): Promise<BuildResult> {
if (workspace) {
Expand Down
50 changes: 50 additions & 0 deletions packages/cli/src/build/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copy of types from `@rnx-kit/tools-apple`. If Jest wasn't such a pain to
* configure, we would have added an `import type` at the top instead:
*
* import type { BuildParams as AndroidBuildParams } from "@rnx-kit/tools-android" with { "resolution-mode": "import" };
* import type { BuildParams as AppleBuildParams } from "@rnx-kit/tools-apple" with { "resolution-mode": "import" };
*
* But Jest doesn't like import attributes and it doesn't matter if we add
* `@babel/plugin-syntax-import-attributes` in the config.
*
* TOOD: Remove this file when we can migrate away from Jest in this package.
*/

export type DeviceType = "device" | "emulator" | "simulator";

export type BuildConfiguration = "Debug" | "Release";

export type AndroidBuildParams = {
platform: "android";
destination?: DeviceType;
configuration?: BuildConfiguration;
archs?: string;
};

export type AppleBuildParams =
| {
platform: "ios" | "visionos";
scheme?: string;
destination?: DeviceType;
configuration?: BuildConfiguration;
archs?: string;
isBuiltRemotely?: boolean;
}
| {
platform: "macos";
scheme?: string;
configuration?: BuildConfiguration;
isBuiltRemotely?: boolean;
};

export type AndroidInputParams = AndroidBuildParams & {
device?: string;
};

export type AppleInputParams = AppleBuildParams & {
device?: string;
workspace?: string;
};

export type InputParams = AndroidInputParams | AppleInputParams;
7 changes: 6 additions & 1 deletion packages/cli/src/run.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Config } from "@react-native-community/cli-types";
import { rnxBuildCommand } from "./build";
import type { InputParams } from "./build/apple";
import type { InputParams } from "./build/types";
import { runAndroid } from "./run/android";
import { runIOS } from "./run/ios";
import { runMacOS } from "./run/macos";

Expand All @@ -10,9 +11,13 @@ export function rnxRun(
buildParams: InputParams
) {
switch (buildParams.platform) {
case "android":
return runAndroid(config, buildParams);

case "ios":
case "visionos":
return runIOS(config, buildParams);

case "macos":
return runMacOS(config, buildParams);
}
Expand Down
68 changes: 68 additions & 0 deletions packages/cli/src/run/android.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Config } from "@react-native-community/cli-types";
import * as path from "node:path";
import ora from "ora";
import { buildAndroid } from "../build/android";
import type { AndroidInputParams } from "../build/types";

export async function runAndroid(
config: Config,
buildParams: AndroidInputParams
) {
const logger = ora();

const projectDir = await buildAndroid(config, buildParams, logger);
if (typeof projectDir !== "string") {
return;
}

logger.start("Preparing to launch app");

const { findOutputFile, getPackageName, install, selectDevice, start } =
await import("@rnx-kit/tools-android");

const { configuration = "Debug" } = buildParams;
const apks = findOutputFile(projectDir, configuration);
if (apks.length === 0) {
logger.fail("Failed to find the APK that was just built");
process.exitCode = 1;
return;
}

if (apks.length > 1) {
const currentStatus = logger.text;
const choices = apks.map((p) => path.basename(p)).join(", ");
logger.info(`Multiple APKs were found; picking the first one: ${choices}`);
logger.info("If this is wrong, remove the others and try again");
logger.start(currentStatus);
}

const apk = apks[0];
const info = getPackageName(apk);
if (info instanceof Error) {
logger.fail(info.message);
process.exitCode = 1;
return;
}

const device = await selectDevice(buildParams.device, logger);
if (!device) {
logger.fail("Failed to launch app: Could not find an appropriate device");
process.exitCode = 1;
return;
}

logger.start(`Installing ${apk}`);

const { packageName, activityName } = info;
const error = await install(device, apk, packageName);
if (error) {
logger.fail(error.message);
process.exitCode = 1;
return;
}

logger.text = `Starting ${packageName}`;
await start(device, packageName, activityName);

logger.succeed(`Started ${packageName}`);
}
6 changes: 3 additions & 3 deletions packages/cli/src/run/ios.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Config } from "@react-native-community/cli-types";
import * as path from "node:path";
import ora from "ora";
import type { InputParams } from "../build/apple";
import { buildIOS } from "../build/ios";
import type { InputParams } from "../build/types";

export async function runIOS(config: Config, buildParams: InputParams) {
const { platform } = buildParams;
Expand All @@ -17,7 +17,7 @@ export async function runIOS(config: Config, buildParams: InputParams) {
return;
}

logger.start("Preparing to launch app...");
logger.start("Preparing to launch app");

const {
getBuildSettings,
Expand Down Expand Up @@ -52,7 +52,7 @@ export async function runIOS(config: Config, buildParams: InputParams) {
settings.buildSettings;
const app = path.join(TARGET_BUILD_DIR, EXECUTABLE_FOLDER_PATH);

logger.start(`Installing '${FULL_PRODUCT_NAME}' on ${device.name}...`);
logger.start(`Installing '${FULL_PRODUCT_NAME}' on ${device.name}`);

const installError = await install(device, app);
if (installError) {
Expand Down
Loading

0 comments on commit c2023a7

Please sign in to comment.