-
Notifications
You must be signed in to change notification settings - Fork 54
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
Add custom fingerprinting and build cache #589
Merged
Merged
Changes from 3 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
a537490
Add custom fingerprinting, add build cache
jakub-gonet 798d2a8
Remove cancel token from fingerprinting, extract fingerprint outside …
jakub-gonet 2f3d350
Add comment about autoinstalling pods
jakub-gonet 8fb90d4
Add explanation, rename config options
jakub-gonet baa2ff5
Remove stale comment
jakub-gonet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -129,15 +129,23 @@ Below is an example of how the `launch.json` file could look like with android v | |
Instead of letting Radon IDE build your app, you can use scripts (`buildScript` option) or [Expo | ||
Application Services (EAS)](https://expo.dev/eas) (`eas` option) to do it. | ||
|
||
The requirement for scripts is to output the absolute path to the built app as the | ||
last line of the standard output. | ||
|
||
Both `buildScript` and `eas` are objects having `ios` and `android` optional | ||
The requirement for scripts is to output the absolute path to the built app as | ||
the last line of the standard output. If custom fingerprint script is used, it | ||
should output fingerprint as the last line of the standard output. When | ||
fingerprint changes between invocations, RN IDE will rebuild the project. The | ||
IDE runs fingerprint quite frequently (i.e., on every file save), so this | ||
process should be fast and avoid over the network communication. | ||
|
||
Both `customBuild` and `eas` are objects having `ios` and `android` optional | ||
keys. You can't specify one platform in both custom script and EAS build | ||
options. | ||
|
||
`buildScript.ios` and `buildScript.android` are string keys, representing custom | ||
command used to build the app. Example below: | ||
`customBuild.ios` and `customBuild.android` have following structure with | ||
optional keys that can be used independently: | ||
- `buildScript` – string, specifies a command used for building. | ||
- `fingerprintScript` – string, specifies a command used for creating fingerprint. | ||
|
||
Example: | ||
```json | ||
{ | ||
"version": "0.2.0", | ||
|
@@ -146,8 +154,12 @@ command used to build the app. Example below: | |
"type": "radon-ide", | ||
"request": "launch", | ||
"name": "Radon IDE panel", | ||
"buildScript": { | ||
"android": "npm run build:ftp-fetch-android" | ||
"customBuild": { | ||
"android": { "buildScript": "npm run build:ftp-fetch-android" } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't a script, so maybe we should rename to |
||
"ios": { | ||
"buildScript": "npm run build:ftp-fetch-ios", | ||
"fingerprintScript": "date '+%Y-%m-%d'" | ||
} | ||
} | ||
} | ||
] | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
161 changes: 161 additions & 0 deletions
161
packages/vscode-extension/src/builders/PlatformBuildCache.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import path from "path"; | ||
import fs from "fs"; | ||
import { createFingerprintAsync } from "@expo/fingerprint"; | ||
import { Logger } from "../Logger"; | ||
import { extensionContext, getAppRootFolder } from "../utilities/extensionContext"; | ||
import { DevicePlatform } from "../common/DeviceManager"; | ||
import { IOSBuildResult } from "./buildIOS"; | ||
import { AndroidBuildResult } from "./buildAndroid"; | ||
import { getLaunchConfiguration } from "../utilities/launchConfiguration"; | ||
import { runFingerprintScript } from "./customBuild"; | ||
import { CancelToken } from "./cancelToken"; | ||
import { calculateMD5 } from "../utilities/common"; | ||
import { BuildResult } from "./BuildManager"; | ||
|
||
const ANDROID_BUILD_CACHE_KEY = "android_build_cache"; | ||
const IOS_BUILD_CACHE_KEY = "ios_build_cache"; | ||
|
||
const IGNORE_PATHS = [ | ||
path.join("android", ".gradle/**/*"), | ||
path.join("android", "build/**/*"), | ||
path.join("android", "app", "build/**/*"), | ||
path.join("ios", "build/**/*"), | ||
"**/node_modules/**/android/.cxx/**/*", | ||
"**/node_modules/**/.gradle/**/*", | ||
"**/node_modules/**/android/build/intermediates/cxx/**/*", | ||
]; | ||
|
||
export type BuildCacheInfo = { | ||
fingerprint: string; | ||
buildHash: string; | ||
buildResult: AndroidBuildResult | IOSBuildResult; | ||
}; | ||
|
||
export class PlatformBuildCache { | ||
static instances: Record<DevicePlatform, PlatformBuildCache | undefined> = { | ||
[DevicePlatform.Android]: undefined, | ||
[DevicePlatform.IOS]: undefined, | ||
}; | ||
|
||
static forPlatform(platform: DevicePlatform): PlatformBuildCache { | ||
if (!this.instances[platform]) { | ||
this.instances[platform] = new PlatformBuildCache(platform); | ||
} | ||
|
||
return this.instances[platform]; | ||
} | ||
|
||
private constructor(private readonly platform: DevicePlatform) {} | ||
|
||
get cacheKey() { | ||
return this.platform === DevicePlatform.Android ? ANDROID_BUILD_CACHE_KEY : IOS_BUILD_CACHE_KEY; | ||
} | ||
|
||
/** | ||
* Passed fingerprint should be calculated at the time build is started. | ||
*/ | ||
public async storeBuild(buildFingerprint: string, build: BuildResult) { | ||
const appPath = await getAppHash(getAppPath(build)); | ||
await extensionContext.workspaceState.update(this.cacheKey, { | ||
fingerprint: buildFingerprint, | ||
buildHash: appPath, | ||
buildResult: build, | ||
}); | ||
} | ||
|
||
public async clearCache() { | ||
await extensionContext.workspaceState.update(this.cacheKey, undefined); | ||
} | ||
|
||
public async getBuild(currentFingerprint: string) { | ||
const cache = extensionContext.workspaceState.get<BuildCacheInfo>(this.cacheKey); | ||
if (!cache) { | ||
Logger.debug("No cached build found."); | ||
return undefined; | ||
} | ||
|
||
const fingerprintsMatch = cache.fingerprint === currentFingerprint; | ||
if (!fingerprintsMatch) { | ||
Logger.info( | ||
`Fingerprint mismatch, cannot use cached build. Old: '${cache.fingerprint}', new: '${currentFingerprint}'.` | ||
); | ||
return undefined; | ||
} | ||
|
||
const build = cache.buildResult; | ||
const appPath = getAppPath(build); | ||
try { | ||
const builtAppExists = fs.existsSync(appPath); | ||
if (!builtAppExists) { | ||
Logger.info("Couldn't use cached build. App artifact not found."); | ||
return undefined; | ||
} | ||
|
||
const appHash = await getAppHash(appPath); | ||
const hashesMatch = appHash === cache.buildHash; | ||
if (hashesMatch) { | ||
Logger.info("Using cached build."); | ||
return build; | ||
} | ||
} catch (e) { | ||
// we only log the error and ignore it to allow new build to start | ||
Logger.error("Error while attempting to load cached build: ", e); | ||
return undefined; | ||
} | ||
} | ||
|
||
public async isCacheStale() { | ||
const currentFingerprint = await this.calculateFingerprint(); | ||
const { fingerprint } = | ||
extensionContext.workspaceState.get<BuildCacheInfo>(this.cacheKey) ?? {}; | ||
|
||
return currentFingerprint !== fingerprint; | ||
} | ||
|
||
public async calculateFingerprint() { | ||
const customFingerprint = await this.calculateCustomFingerprint(); | ||
|
||
if (customFingerprint) { | ||
return customFingerprint; | ||
} | ||
|
||
const fingerprint = await createFingerprintAsync(getAppRootFolder(), { | ||
ignorePaths: IGNORE_PATHS, | ||
}); | ||
Logger.log(`Workspace fingerprint: '${fingerprint.hash}'`); | ||
return fingerprint.hash; | ||
} | ||
|
||
private async calculateCustomFingerprint() { | ||
const { customBuild, env } = getLaunchConfiguration(); | ||
const configPlatform = ( | ||
{ | ||
[DevicePlatform.Android]: "android", | ||
[DevicePlatform.IOS]: "ios", | ||
} as const | ||
)[this.platform]; | ||
const fingerprintScript = customBuild?.[configPlatform]?.fingerprintScript; | ||
|
||
if (!fingerprintScript) { | ||
return undefined; | ||
} | ||
|
||
Logger.log(`Using custom fingerprint script '${fingerprintScript}'`); | ||
const fingerprint = await runFingerprintScript(fingerprintScript, env); | ||
|
||
if (!fingerprint) { | ||
throw new Error("Failed to generate workspace fingerprint using custom script."); | ||
} | ||
|
||
Logger.log("Workspace fingerprint", fingerprint); | ||
return fingerprint; | ||
} | ||
} | ||
|
||
function getAppPath(build: BuildResult) { | ||
return build.platform === DevicePlatform.Android ? build.apkPath : build.appPath; | ||
} | ||
|
||
async function getAppHash(appPath: string) { | ||
return (await calculateMD5(appPath)).digest("hex"); | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should explain the concept of fingerprint here. Specifically that it is a string that allows us to tell whether new native build needs to be run