Skip to content

Commit fe29de6

Browse files
committed
fix(cli): add support for building Windows apps
1 parent e8bbe48 commit fe29de6

File tree

16 files changed

+396
-171
lines changed

16 files changed

+396
-171
lines changed

.changeset/calm-spoons-live.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@rnx-kit/cli": patch
3+
"@rnx-kit/tools-windows": patch
4+
---
5+
6+
Add support for building Windows apps

packages/cli/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"@rnx-kit/tools-language": "^3.0.0",
5757
"@rnx-kit/tools-node": "^3.0.0",
5858
"@rnx-kit/tools-react-native": "^2.0.3",
59+
"@rnx-kit/tools-windows": "^0.2.0",
5960
"commander": "^11.1.0",
6061
"ora": "^5.4.1",
6162
"qrcode": "^1.5.0"
@@ -76,6 +77,7 @@
7677
"@babel/core": "^7.20.0",
7778
"@babel/preset-env": "^7.20.0",
7879
"@react-native-community/cli-types": "^20.0.0",
80+
"@react-native-windows/cli": "^0.79.0",
7981
"@rnx-kit/eslint-config": "*",
8082
"@rnx-kit/jest-preset": "*",
8183
"@rnx-kit/scripts": "*",

packages/cli/src/build.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
DeviceType,
1111
InputParams,
1212
} from "./build/types";
13+
import { buildWindows } from "./build/windows";
1314

1415
function asConfiguration(configuration: string): BuildConfiguration {
1516
switch (configuration) {
@@ -42,10 +43,11 @@ function asSupportedPlatform(platform: string): InputParams["platform"] {
4243
case "ios":
4344
case "macos":
4445
case "visionos":
46+
case "windows":
4547
return platform;
4648
default:
4749
throw new InvalidArgumentError(
48-
"Supported platforms: 'android', 'ios', 'macos', 'visionos'."
50+
"Supported platforms: 'android', 'ios', 'macos', 'visionos', 'windows'."
4951
);
5052
}
5153
}
@@ -65,6 +67,9 @@ export function rnxBuild(
6567

6668
case "macos":
6769
return buildMacOS(config, buildParams);
70+
71+
case "windows":
72+
return buildWindows(config, buildParams, argv);
6873
}
6974
}
7075

@@ -82,6 +87,11 @@ export const rnxBuildCommand = {
8287
description: "Target platform",
8388
parse: asSupportedPlatform,
8489
},
90+
{
91+
name: "--solution <string>",
92+
description:
93+
"Path, relative to project root, of the Visual Studio solution to build (Windows only)",
94+
},
8595
{
8696
name: "--workspace <string>",
8797
description:

packages/cli/src/build/ios.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export function buildIOS(
1212
logger = ora()
1313
): Promise<BuildResult> {
1414
const { platform } = buildParams;
15+
if (process.platform !== "darwin") {
16+
logger.fail(`${platform} builds can only be performed on macOS hosts`);
17+
return Promise.resolve(1);
18+
}
19+
1520
const { sourceDir, xcodeProject } = config.project[platform] ?? {};
1621
if (!sourceDir || !xcodeProject) {
1722
invalidateState();

packages/cli/src/build/macos.ts

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,21 @@ import type { BuildResult } from "./apple";
77
import { runBuild } from "./apple";
88
import type { AppleInputParams } from "./types";
99

10-
function findXcodeWorkspaces(searchDir: string) {
11-
return fs.existsSync(searchDir)
10+
function findXcodeWorkspaces(
11+
searchDir: string,
12+
logger: ora.Ora
13+
): string | undefined {
14+
const workspaces = fs.existsSync(searchDir)
1215
? fs.readdirSync(searchDir).filter((file) => file.endsWith(".xcworkspace"))
1316
: [];
14-
}
15-
16-
export function buildMacOS(
17-
_config: Config,
18-
{ workspace, ...buildParams }: AppleInputParams,
19-
logger = ora()
20-
): Promise<BuildResult> {
21-
if (workspace) {
22-
return runBuild(workspace, buildParams, logger);
23-
}
2417

25-
const sourceDir = "macos";
26-
const workspaces = findXcodeWorkspaces(sourceDir);
2718
if (workspaces.length === 0) {
2819
invalidateState();
2920
process.exitCode = 1;
3021
logger.fail(
3122
"No Xcode workspaces were found; specify an Xcode workspace with `--workspace`"
3223
);
33-
return Promise.resolve(1);
24+
return undefined;
3425
}
3526

3627
if (workspaces.length > 1) {
@@ -42,5 +33,24 @@ export function buildMacOS(
4233
);
4334
}
4435

45-
return runBuild(path.join(sourceDir, workspaces[0]), buildParams, logger);
36+
return path.join(searchDir, workspaces[0]);
37+
}
38+
39+
export function buildMacOS(
40+
_config: Config,
41+
{ workspace, ...buildParams }: AppleInputParams,
42+
logger = ora()
43+
): Promise<BuildResult> {
44+
if (process.platform !== "darwin") {
45+
logger.fail("macOS builds can only be performed on macOS hosts");
46+
return Promise.resolve(1);
47+
}
48+
49+
const sourceDir = "macos";
50+
const xcworkspace = workspace || findXcodeWorkspaces(sourceDir, logger);
51+
if (!xcworkspace) {
52+
return Promise.resolve(1);
53+
}
54+
55+
return runBuild(xcworkspace, buildParams, logger);
4656
}

packages/cli/src/build/types.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,17 @@ export type AppleInputParams = AppleBuildParams & {
4747
workspace?: string;
4848
};
4949

50-
export type InputParams = AndroidInputParams | AppleInputParams;
50+
export type WindowsBuildParams = {
51+
platform: "windows";
52+
configuration?: BuildConfiguration;
53+
architecture?: "arm64" | "x64";
54+
};
55+
56+
export type WindowsInputParams = WindowsBuildParams & {
57+
solution?: string;
58+
};
59+
60+
export type InputParams =
61+
| AndroidInputParams
62+
| AppleInputParams
63+
| WindowsInputParams;

packages/cli/src/build/windows.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import type { Command, Config } from "@react-native-community/cli-types";
2+
import { invalidateState } from "@rnx-kit/tools-react-native/cache";
3+
import * as fs from "node:fs";
4+
import * as os from "node:os";
5+
import * as path from "node:path";
6+
import ora from "ora";
7+
import type { WindowsBuildParams, WindowsInputParams } from "./types";
8+
import { watch } from "./watcher";
9+
10+
export type BuildArgs = {
11+
solution: string;
12+
args: string[];
13+
};
14+
15+
export type BuildResult = BuildArgs | number | null;
16+
17+
function findRunCommand(startDir: string): Command | undefined {
18+
try {
19+
const fromProjectRoot = { paths: [startDir] };
20+
const rnwPath = require.resolve(
21+
"react-native-windows/package.json",
22+
fromProjectRoot
23+
);
24+
25+
const fromRnwDir = { paths: [path.dirname(rnwPath)] };
26+
const cliPath = require.resolve("@react-native-windows/cli", fromRnwDir);
27+
28+
const cli = require(cliPath) as typeof import("@react-native-windows/cli");
29+
return cli.commands.find((cmd) => cmd.name === "run-windows");
30+
} catch (_) {
31+
// Handled by caller
32+
}
33+
34+
return undefined;
35+
}
36+
37+
function findSolution(searchDir: string, logger: ora.Ora): string | undefined {
38+
const solutions = fs.existsSync(searchDir)
39+
? fs.readdirSync(searchDir).filter((file) => file.endsWith(".sln"))
40+
: [];
41+
42+
if (solutions.length === 0) {
43+
invalidateState();
44+
process.exitCode = 1;
45+
logger.fail(
46+
"No Visual Studio solutions were found; specify a Visual Studio solution with `--solution`"
47+
);
48+
return undefined;
49+
}
50+
51+
if (solutions.length > 1) {
52+
logger.fail(
53+
`Multiple Visual Studio solutions were found; picking the first one: ${solutions.join(", ")}`
54+
);
55+
logger.fail("If this is wrong, specify another solution with `--solution`");
56+
}
57+
58+
return path.join(searchDir, solutions[0]);
59+
}
60+
61+
export function runBuild(
62+
solution: string,
63+
buildParams: WindowsBuildParams,
64+
additionalArgs: string[],
65+
logger: ora.Ora
66+
): Promise<BuildResult> {
67+
return import("@rnx-kit/tools-windows").then(({ buildAppxBundle }) => {
68+
const build = buildAppxBundle(solution, buildParams, additionalArgs);
69+
return watch(build, logger, () => ({
70+
solution,
71+
args: build.spawnargs,
72+
}));
73+
});
74+
}
75+
76+
function toRunWindowsOptions(
77+
sln: string,
78+
{ root }: Config,
79+
{ configuration, architecture }: WindowsBuildParams
80+
) {
81+
return {
82+
release: configuration === "Release",
83+
root,
84+
arch: architecture ?? os.arch(),
85+
packager: false,
86+
bundle: false,
87+
sln,
88+
};
89+
}
90+
91+
export function buildWindows(
92+
config: Config,
93+
params: WindowsInputParams,
94+
additionalArgs: string[],
95+
logger = ora()
96+
): Promise<BuildResult> {
97+
if (process.platform !== "win32") {
98+
logger.fail("Windows builds can only be performed on Windows hosts");
99+
return Promise.resolve(1);
100+
}
101+
102+
const sourceDir = "windows";
103+
const solution = params.solution || findSolution(sourceDir, logger);
104+
if (!solution) {
105+
return Promise.resolve(1);
106+
}
107+
108+
const runCommand = findRunCommand(config.root);
109+
if (!runCommand) {
110+
logger.fail(
111+
"Failed to find `@react-native-windows/cli`, make sure `react-native-windows` is installed."
112+
);
113+
return Promise.resolve(1);
114+
}
115+
116+
const options = toRunWindowsOptions(solution, config, params);
117+
const build = runCommand.func(additionalArgs, config, options);
118+
return build
119+
? build.then(() => ({ solution, args: additionalArgs }))
120+
: Promise.resolve(1);
121+
}

packages/cli/src/run.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export function rnxRun(
2121

2222
case "macos":
2323
return runMacOS(config, buildParams);
24+
25+
case "windows":
26+
throw new Error("Running Windows apps is not yet supported.");
27+
//return runWindows(config, buildParams);
2428
}
2529
}
2630

packages/test-app-windows/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"format": "rnx-kit-scripts format",
2626
"lint": "rnx-kit-scripts lint",
2727
"start": "rnx start",
28-
"windows": "rnx run-windows --no-packager --sln windows/SampleCrossApp.sln"
28+
"windows": "rnx run --platform windows"
2929
},
3030
"dependencies": {
3131
"@react-native-webapis/web-storage": "workspace:*",

packages/tools-windows/README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ import * as tools from "@rnx-kit/tools-windows";
1515
<!-- The following table can be updated by running `yarn update-readme` -->
1616
<!-- @rnx-kit/api start -->
1717

18-
| Category | Function | Description |
19-
| -------- | ---------------------------- | ----------------------------------------------------------------------- |
20-
| - | `getPackageInfo(app)` | Returns information about the app package at specified path. |
21-
| - | `install(app, tryUninstall)` | Installs the app package at specified path, and returns its identifier. |
22-
| - | `start(packageId)` | Starts the app with specified identifier. |
18+
| Category | Function | Description |
19+
| -------- | --------------------------------------------------- | ----------------------------------------------------------------------- |
20+
| apps | `getPackageInfo(app)` | Returns information about the app package at specified path. |
21+
| apps | `install(app, tryUninstall)` | Installs the app package at specified path, and returns its identifier. |
22+
| apps | `start(packageId)` | Starts the app with specified identifier. |
23+
| msbuild | `buildAppxBundle(solution, params, additionalArgs)` | Builds an AppX bundle for the specified solution using MSBuild. |
2324

2425
<!-- @rnx-kit/api end -->

0 commit comments

Comments
 (0)