Skip to content
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
8d372ef
tweak publish config
mdjastrzebski Dec 18, 2024
41fd9f0
improve
mdjastrzebski Dec 18, 2024
c598e14
simplify script
mdjastrzebski Dec 18, 2024
44f07f3
adapt gh workflows
mdjastrzebski Dec 19, 2024
c6abeff
.
mdjastrzebski Dec 19, 2024
9bcde38
post only when in PR
mdjastrzebski Dec 19, 2024
55b2f57
.
mdjastrzebski Dec 19, 2024
b8adcfe
.
mdjastrzebski Dec 19, 2024
a629730
.
mdjastrzebski Dec 19, 2024
b15a453
detect project root and ci
mdjastrzebski Dec 12, 2024
a4e6a93
fetch artifact (wip)
mdjastrzebski Dec 13, 2024
46298ac
detect GH remote repo details
mdjastrzebski Dec 17, 2024
cd35fb2
cached build workflow
mdjastrzebski Dec 19, 2024
75227b1
query and download artifact
mdjastrzebski Dec 19, 2024
fb53dfc
update actions
mdjastrzebski Dec 19, 2024
e46a9e0
cleanup logs
mdjastrzebski Dec 19, 2024
3295a59
.
mdjastrzebski Dec 19, 2024
d0798dc
unzip downloaded archive to apk
thymikee Dec 19, 2024
e922395
adjust wording
thymikee Dec 20, 2024
c5e3d57
cleanup zip code
mdjastrzebski Dec 20, 2024
e4ec1a2
eslint ignore dist
mdjastrzebski Dec 20, 2024
4303f7a
.
mdjastrzebski Dec 20, 2024
1247f1f
local caching android
mdjastrzebski Dec 20, 2024
f549041
extract fetchCachedBuild fn
thymikee Dec 21, 2024
25f0941
remove unnecessary binaryPath check
thymikee Dec 21, 2024
98b23ca
parametrize fetchCachedBuild with ci provider
thymikee Dec 21, 2024
06ae444
build android action improvements
mdjastrzebski Dec 23, 2024
4146774
fix ios artifact packing
mdjastrzebski Dec 23, 2024
b930a38
ios fetch cached build
mdjastrzebski Dec 23, 2024
fe246a2
Merge branch 'main' into feat/remote-cache-build
mdjastrzebski Dec 24, 2024
db611b3
remote build caching on iOS simu!
mdjastrzebski Dec 24, 2024
2c90119
use nano-spawn
thymikee Dec 23, 2024
bbda187
update expo fingerprint
mdjastrzebski Dec 24, 2024
1e412b5
fix test
mdjastrzebski Dec 24, 2024
02be174
fix e2e
mdjastrzebski Dec 24, 2024
2c4db97
refactor android
mdjastrzebski Dec 27, 2024
cb0c895
refactor ios
mdjastrzebski Dec 27, 2024
fe6e16c
self code review
mdjastrzebski Dec 27, 2024
0471743
fix test
mdjastrzebski Dec 27, 2024
fa9a202
fix tests, add `--no-remote-build-cache` CLI option
mdjastrzebski Dec 27, 2024
dfbf966
Merge branch 'main' into feat/remote-cache-build
mdjastrzebski Dec 31, 2024
ff4c3ea
code review changes
mdjastrzebski Dec 31, 2024
2c8d0cc
.
mdjastrzebski Dec 31, 2024
0be05fb
.
mdjastrzebski Dec 31, 2024
7038dd8
.
mdjastrzebski Dec 31, 2024
e466d10
.
mdjastrzebski Dec 31, 2024
82c6268
.
mdjastrzebski Dec 31, 2024
1c6c4b8
.
mdjastrzebski Dec 31, 2024
e00317a
git ignore
mdjastrzebski Dec 31, 2024
794d2a8
Merge branch 'main' into feat/remote-cache-build
mdjastrzebski Dec 31, 2024
54ed84f
.
mdjastrzebski Dec 31, 2024
0ed7359
tweak cache location
mdjastrzebski Jan 2, 2025
f59febd
.
mdjastrzebski Jan 2, 2025
b35c6b3
Merge branch 'main' into feat/remote-cache-build
mdjastrzebski Jan 2, 2025
91d72e6
fix lockfile
mdjastrzebski Jan 2, 2025
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
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ NPM_CONFIG_REGISTRY=http://localhost:4873 pnpm create @rnef/app --registry http:
# Then use pnpm install with registry
echo "node-linker=hoisted" > .npmrc
NPM_CONFIG_REGISTRY=http://localhost:4873 pnpm install
# Then link packages (see above)
pnpm link --global ...

# Clean up
pnpm verdaccio-reset
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default [
rules: {},
},
{
ignores: ['**/template/**/*.mjs'],
ignores: ['**/template/**/*.mjs', '**/dist/**'],
},
{
files: ['**/*.json'],
Expand Down
10 changes: 3 additions & 7 deletions packages/cli/src/lib/commands/fingerprint.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { performance } from 'perf_hooks';
import { outro, spinner } from '@clack/prompts';
import {
logger,
nativeFingerprint,
resolveAbsolutePath,
} from '@rnef/tools';
import { logger, nativeFingerprint } from '@rnef/tools';

type NativeFingerprintCommandOptions = {
platform: 'ios' | 'android';
Expand All @@ -16,14 +12,13 @@ export async function nativeFingerprintCommand(
) {
path = path ?? '.';
const platform = options?.platform ?? 'ios';
const loader = spinner();
logger.debug(`Fingerprinting "${resolveAbsolutePath(path)}"...`);

let start = 0;
if (logger.isVerbose()) {
start = performance.now();
}

const loader = spinner();
loader.start("Calculating fingerprint for the project's native parts");
const fingerprint = await nativeFingerprint(path, { platform });

Expand All @@ -33,6 +28,7 @@ export async function nativeFingerprintCommand(
logger.debug('Sources:', JSON.stringify(fingerprint.sources, null, 2));
logger.debug(`Duration: ${(duration / 1000).toFixed(1)}s`);
}

loader.stop(`Fingerprint calculated: ${fingerprint.hash}`);
outro('Success 🎉.');
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const promptForTaskSelection = async (
): Promise<string> => {
const tasks = await getGradleTasks(taskType, sourceDir);
if (!tasks.length) {
throw new Error(`No actionable ${taskType} tasks were found...`);
throw new Error(`No actionable ${taskType} tasks were found`);
}
const task = checkCancelPrompt<string>(
await select({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import fs, { PathLike } from 'node:fs';
import { vi, test, Mock } from 'vitest';
import { logger } from '@rnef/tools';
import { AndroidProjectConfig } from '@react-native-community/cli-types';
import { select } from '@clack/prompts';
import spawn from 'nano-spawn';
import { runAndroid, type Flags } from '../runAndroid.js';
import { logger } from '@rnef/tools';

const actualFs = await vi.importMock('node:fs');

Expand Down Expand Up @@ -35,6 +35,9 @@ vi.mock('@clack/prompts', () => {
isCancel: vi.fn(() => false),
intro: vi.fn(),
outro: mocks.outroMock,
log: {
warn: vi.fn(),
},
};
});

Expand All @@ -48,6 +51,7 @@ const args: Flags = {
appIdSuffix: '',
mainActivity: undefined,
port: '8081',
remoteBuildCache: false,
};
const androidProject: AndroidProjectConfig = {
appName: 'app',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
LocalBuild,
createRemoteBuildCache,
findFilesWithPattern,
formatArtifactName,
getProjectRoot,
nativeFingerprint,
queryLocalBuildCache,
} from '@rnef/tools';
import { spinner } from '@clack/prompts';
import path from 'node:path';
import color from 'picocolors';

export type FetchCachedBuildOptions = {
mode: string;
};

export async function fetchCachedBuild({
mode,
}: FetchCachedBuildOptions): Promise<LocalBuild | null> {
const loader = spinner();
loader.start('Looking for a local cached build');

const root = getProjectRoot();
const artifactName = await calculateArtifactName(mode);

const localBuild = queryLocalBuildCache(artifactName, { findBinary });
if (localBuild != null) {
loader.stop(
`Found local cached build: ${color.cyan(
path.relative(root, localBuild.binaryPath)
)}`
);
return localBuild;
}

const remoteBuildCache = createRemoteBuildCache();
if (!remoteBuildCache) {
loader.stop(`No CI provider detected, skipping.`);
return null;
}

loader.message(`Looking for a cached build on ${remoteBuildCache.name}`);
const remoteBuild = await remoteBuildCache.query(artifactName);
if (!remoteBuild) {
loader.stop(`No cached build found for "${artifactName}".`);
return null;
}

loader.message(`Downloading cached build from ${remoteBuildCache.name}`);
const fetchedBuild = await remoteBuildCache.fetch(remoteBuild);
const binaryPath = findBinary(fetchedBuild.path);
if (!binaryPath) {
loader.stop(`No binary found in "${artifactName}".`);
return null;
}

loader.stop(
`Downloaded cached build: ${color.cyan(path.relative(root, binaryPath))}.`
);

return {
name: fetchedBuild.name,
artifactPath: fetchedBuild.path,
binaryPath,
};
}

async function calculateArtifactName(mode: string) {
const root = getProjectRoot();
const fingerprint = await nativeFingerprint(root, { platform: 'android' });
return formatArtifactName({
platform: 'android',
mode,
hash: fingerprint.hash,
});
}

function findBinary(path: string): string | null {
const apks = findFilesWithPattern(path, /\.apk$/);
if (apks.length > 0) {
return apks[0];
}

const aabs = findFilesWithPattern(path, /\.aab$/);
if (aabs.length > 0) {
return aabs[0];
}

return null;
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import fs from 'fs';
import fs from 'node:fs';
import {
AndroidProjectConfig,
Config,
} from '@react-native-community/cli-types';
import { checkCancelPrompt, logger } from '@rnef/tools';
import { intro, outro, select } from '@clack/prompts';
import isInteractive from 'is-interactive';
import { getDevices } from './adb.js';
import { toPascalCase } from '../toPascalCase.js';
import { tryLaunchAppOnDevice } from './tryLaunchAppOnDevice.js';
import { tryInstallAppOnDevice } from './tryInstallAppOnDevice.js';
import { listAndroidDevices, DeviceData } from './listAndroidDevices.js';
import { tryLaunchEmulator } from './tryLaunchEmulator.js';
import path from 'path';
import path from 'node:path';
import { BuildFlags, options } from '../buildAndroid/buildAndroid.js';
import { promptForTaskSelection } from '../listAndroidTasks.js';
import { runGradle } from '../runGradle.js';
import { outro, select } from '@clack/prompts';
import isInteractive from 'is-interactive';
import { fetchCachedBuild } from './fetchCachedBuild.js';

export interface Flags extends BuildFlags {
appId: string;
Expand All @@ -25,6 +26,7 @@ export interface Flags extends BuildFlags {
device?: string;
binaryPath?: string;
user?: string;
remoteBuildCache: boolean;
}

export type AndroidProject = NonNullable<Config['project']['android']>;
Expand All @@ -37,6 +39,8 @@ export async function runAndroid(
args: Flags,
projectRoot: string
) {
intro('Running Android app');

normalizeArgs(args, projectRoot);

const { deviceId } = args.interactive
Expand All @@ -48,6 +52,14 @@ export async function runAndroid(
? [await promptForTaskSelection(mainTaskType, androidProject.sourceDir)]
: [...(args.tasks ?? []), `${mainTaskType}${toPascalCase(args.mode)}`];

if (!args.binaryPath && args.remoteBuildCache) {
const cachedBuild = await fetchCachedBuild({ mode: args.mode });
if (cachedBuild) {
// @todo replace with a more generic way to pass binary path
args.binaryPath = cachedBuild.binaryPath;
}
}

if (deviceId) {
await runGradle({ tasks, androidProject, args });
if (!(await getDevices()).find((d) => d === deviceId)) {
Expand All @@ -64,7 +76,7 @@ export async function runAndroid(
await selectAndLaunchDevice();
} else {
logger.debug(
'No booted devices or emulators found. Launching first available mulator...'
'No booted devices or emulators found. Launching first available emulator...'
);
await tryLaunchEmulator();
}
Expand All @@ -73,6 +85,9 @@ export async function runAndroid(
await runGradle({ tasks, androidProject, args });

for (const device of await getDevices()) {
if (args.binaryPath) {
await tryInstallAppOnDevice(device, androidProject, args, tasks);
}
await tryLaunchAppOnDevice(device, androidProject, args);
}
}
Expand Down Expand Up @@ -195,4 +210,8 @@ export const runOptions = [
name: '--user <number>',
description: 'Id of the User Profile you want to install the app on.',
},
{
name: '--no-remote-build-cache',
description: 'Do not use remote build cacheing.',
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function tryInstallAppOnDevice(

const adbPath = getAdbPath();
const loader = spinner();
loader.start(`Installing the app on "${device}"...`);
loader.start(`Installing the app on "${device}"`);
const { stderr } = await spawn(adbPath, adbArgs, {
stdio: ['ignore', 'ignore', 'pipe'],
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function tryLaunchAppOnDevice(
const adbPath = getAdbPath();
logger.debug(`Running ${adbPath} ${adbArgs.join(' ')}.`);
const loader = spinner();
loader.start(`Installing the app on "${device}"...`);
loader.start(`Installing the app on "${device}"`);
const { stderr } = await spawn(adbPath, adbArgs, {
stdio: ['ignore', 'ignore', 'pipe'],
});
Expand Down
12 changes: 7 additions & 5 deletions packages/plugin-platform-android/src/lib/commands/runGradle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import spawn from 'nano-spawn';
import type { BuildFlags } from './buildAndroid/buildAndroid.js';
import { spinner } from '@clack/prompts';

export type RunGradleArgs = {
tasks: string[];
androidProject: AndroidProject;
args: BuildFlags | Flags;
};

export async function runGradle({
tasks,
androidProject,
args,
}: {
tasks: string[];
androidProject: AndroidProject;
args: BuildFlags | Flags;
}) {
}: RunGradleArgs) {
if ('binaryPath' in args) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,10 @@ inputs:
description: "Path to the build output artifact"
required: false
default: "android/app/build/outputs/apk/debug/app-debug.apk"
artifact-name-prefix:
description: "Prefix for the artifact name"
mode:
description: "Build mode"
required: false
default: "app-debug"
artifact-name-extension:
description: "File extension for the artifact name"
required: false
default: "apk"
default: "debug"

outputs:
artifact-url:
Expand All @@ -34,13 +30,18 @@ runs:
id: fingerprint
uses: ./.github/actions/rnef-native-fingerprint
with:
platforms: android
platform: android

- name: Set artifact name
run: |
echo "ARTIFACT_NAME=rnef-android-${{ inputs.mode }}-${{ steps.fingerprint.outputs.hash}}" >> $GITHUB_ENV
shell: bash

- name: Find artifact URL
id: cached-url
uses: ./.github/actions/find-artifact
with:
name: ${{inputs.artifact-name-prefix}}-${{ steps.fingerprint.outputs.hash}}.${{inputs.artifact-name-extension}}
name: ${{ env.ARTIFACT_NAME }}

- name: Post Cached Build (if found)
if: ${{ steps.cached-url.outputs.artifact-url && github.event_name == 'pull_request' }}
Expand Down Expand Up @@ -68,12 +69,12 @@ runs:
npx rnef build:android --tasks assembleDebug
shell: bash

- name: Upload APK
- name: Upload Artifact
id: upload-artifact
if: ${{ !steps.cached-url.outputs.artifact-url }}
uses: actions/upload-artifact@v4
with:
name: ${{inputs.artifact-name-prefix}}-${{ steps.fingerprint.outputs.hash}}.${{inputs.artifact-name-extension}}
name: ${{ env.ARTIFACT_NAME }}
path: ${{ inputs.artifact-output-path }}
if-no-files-found: error

Expand Down
1 change: 1 addition & 0 deletions packages/plugin-platform-apple/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"is-interactive": "^2.0.0",
"nano-spawn": "^0.2.0",
"picocolors": "^1.1.1",
"tar": "^7.4.3",
"tslib": "^2.3.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '../../types/index.js';
import { RunFlags } from './runOptions.js';
import { selectFromInteractiveMode } from '../../utils/selectFromInteractiveMode.js';
import { outro, spinner } from '@clack/prompts';
import { intro, outro, spinner } from '@clack/prompts';
import { runOnMac } from './runOnMac.js';
import { runOnMacCatalyst } from './runOnMacCatalyst.js';
import { cacheRecentDevice } from './recentDevices.js';
Expand All @@ -28,6 +28,8 @@ export const createRun = async (
args: RunFlags,
projectRoot: string
) => {
intro('Running on iOS');

const { readableName: platformReadableName } = getPlatformInfo(platformName);
const { xcodeProject, sourceDir } = projectConfig;

Expand Down
Loading
Loading