Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tools-android): add primitives for build and run commands #3322

Merged
merged 1 commit into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";
tido64 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading