-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Integrate FAH Local Builds with Universal Maker #10382
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
base: next
Are you sure you want to change the base?
Changes from all commits
c573c6a
67cc1cc
a7cf61e
a2bce07
80f5aa1
0bb1d5c
0bccdc2
152505d
d2185aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,14 +1,152 @@ | ||||||||||||||||||||||||||||||||||
| import { BuildConfig, Env } from "../gcp/apphosting"; | ||||||||||||||||||||||||||||||||||
| import * as childProcess from "child_process"; | ||||||||||||||||||||||||||||||||||
| import * as fs from "fs"; | ||||||||||||||||||||||||||||||||||
| import * as path from "path"; | ||||||||||||||||||||||||||||||||||
| import { Availability, BuildConfig, Env } from "../gcp/apphosting"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import { localBuild as localAppHostingBuild } from "@apphosting/build"; | ||||||||||||||||||||||||||||||||||
| import { EnvMap } from "./yaml"; | ||||||||||||||||||||||||||||||||||
| import { loadSecret } from "./secrets"; | ||||||||||||||||||||||||||||||||||
| import { confirm } from "../prompt"; | ||||||||||||||||||||||||||||||||||
| import { FirebaseError } from "../error"; | ||||||||||||||||||||||||||||||||||
| import * as experiments from "../experiments"; | ||||||||||||||||||||||||||||||||||
| import { logger } from "../logger"; | ||||||||||||||||||||||||||||||||||
| import { wrappedSafeLoad } from "../utils"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
Check failure on line 15 in src/apphosting/localbuilds.ts
|
||||||||||||||||||||||||||||||||||
| interface UniversalMakerOutput { | ||||||||||||||||||||||||||||||||||
| command: string; | ||||||||||||||||||||||||||||||||||
| args: string[]; | ||||||||||||||||||||||||||||||||||
| language: string; | ||||||||||||||||||||||||||||||||||
| runtime: string; | ||||||||||||||||||||||||||||||||||
| envVars?: Record<string, string | number | boolean>; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Runs the Universal Maker binary to build the project. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| export function runUniversalMaker(projectRoot: string, framework?: string): AppHostingBuildOutput { | ||||||||||||||||||||||||||||||||||
| if (!process.env.UNIVERSAL_MAKER_BINARY) { | ||||||||||||||||||||||||||||||||||
| throw new FirebaseError( | ||||||||||||||||||||||||||||||||||
| "Please specify the path to your Universal Maker binary by establishing the UNIVERSAL_MAKER_BINARY environment variable.", | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| childProcess.spawnSync( | ||||||||||||||||||||||||||||||||||
| process.env.UNIVERSAL_MAKER_BINARY, | ||||||||||||||||||||||||||||||||||
| ["-application_dir", projectRoot, "-output_dir", projectRoot, "-output_format", "json"], | ||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||
| env: { | ||||||||||||||||||||||||||||||||||
| ...process.env, | ||||||||||||||||||||||||||||||||||
| X_GOOGLE_TARGET_PLATFORM: "fah", | ||||||||||||||||||||||||||||||||||
| FIREBASE_OUTPUT_BUNDLE_DIR: "bundle_output", | ||||||||||||||||||||||||||||||||||
| NPM_CONFIG_REGISTRY: "https://registry.npmjs.org/", | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| stdio: "inherit", | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+35
to
+47
Contributor
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. The exit status of the const result = childProcess.spawnSync(
process.env.UNIVERSAL_MAKER_BINARY,
["-application_dir", projectRoot, "-output_dir", projectRoot, "-output_format", "json"],
{
env: {
...process.env,
X_GOOGLE_TARGET_PLATFORM: "fah",
FIREBASE_OUTPUT_BUNDLE_DIR: "bundle_output",
},
stdio: "inherit",
},
);
if (result.status !== 0) {
throw new FirebaseError(`Universal Maker build failed with status ${result.status}`, {
exit: result.status ?? 1,
});
} |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Universal Maker has a bug where it accidentally empties bundle.yaml if we tell it to output directly to .apphosting. | ||||||||||||||||||||||||||||||||||
| // To avoid this, we output to bundle_output first, and then safely move the files over. | ||||||||||||||||||||||||||||||||||
| const bundleOutput = path.join(projectRoot, "bundle_output"); | ||||||||||||||||||||||||||||||||||
| const targetAppHosting = path.join(projectRoot, ".apphosting"); | ||||||||||||||||||||||||||||||||||
| if (fs.existsSync(bundleOutput)) { | ||||||||||||||||||||||||||||||||||
| if (!fs.existsSync(targetAppHosting)) { | ||||||||||||||||||||||||||||||||||
| fs.mkdirSync(targetAppHosting, { recursive: true }); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| const files = fs.readdirSync(bundleOutput); | ||||||||||||||||||||||||||||||||||
| for (const file of files) { | ||||||||||||||||||||||||||||||||||
| fs.renameSync(path.join(bundleOutput, file), path.join(targetAppHosting, file)); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| fs.rmdirSync(bundleOutput); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+53
to
+62
Contributor
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. The if (fs.existsSync(bundleOutput)) {
fs.rmSync(targetAppHosting, { recursive: true, force: true });
fs.mkdirSync(targetAppHosting, { recursive: true });
const files = fs.readdirSync(bundleOutput);
for (const file of files) {
fs.renameSync(path.join(bundleOutput, file), path.join(targetAppHosting, file));
}
fs.rmSync(bundleOutput, { recursive: true, force: true });
} |
||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||
| if (e && typeof e === "object" && "code" in e && e.code === "EACCES") { | ||||||||||||||||||||||||||||||||||
| throw new FirebaseError( | ||||||||||||||||||||||||||||||||||
| "Failed to execute the Universal Maker binary due to permission constraints. Please assure you have set chmod +x on your file.", | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| throw e; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const outputFilePath = path.join(projectRoot, "build_output.json"); | ||||||||||||||||||||||||||||||||||
| if (!fs.existsSync(outputFilePath)) { | ||||||||||||||||||||||||||||||||||
| throw new FirebaseError( | ||||||||||||||||||||||||||||||||||
| `Universal Maker did not produce the expected output file at ${outputFilePath}`, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const outputRaw = fs.readFileSync(outputFilePath, "utf-8"); | ||||||||||||||||||||||||||||||||||
| let umOutput: UniversalMakerOutput; | ||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| umOutput = JSON.parse(outputRaw) as UniversalMakerOutput; | ||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||
| throw new FirebaseError(`Failed to parse build_output.json: ${(e as Error).message}`); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+79
to
+85
Contributor
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. The
Suggested change
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let finalRunCommand = `${umOutput.command} ${umOutput.args.join(" ")}`; | ||||||||||||||||||||||||||||||||||
| const bundleYamlPath = path.join(projectRoot, ".apphosting", "bundle.yaml"); | ||||||||||||||||||||||||||||||||||
| if (fs.existsSync(bundleYamlPath)) { | ||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| const bundleRaw = fs.readFileSync(bundleYamlPath, "utf-8"); | ||||||||||||||||||||||||||||||||||
| // Safely parse the YAML string | ||||||||||||||||||||||||||||||||||
| const bundleData = wrappedSafeLoad(bundleRaw); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (bundleData?.runConfig?.runCommand) { | ||||||||||||||||||||||||||||||||||
| finalRunCommand = bundleData.runConfig.runCommand; | ||||||||||||||||||||||||||||||||||
|
Check warning on line 96 in src/apphosting/localbuilds.ts
|
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||
| // Fall back gracefully if parser fails | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| metadata: { | ||||||||||||||||||||||||||||||||||
| language: umOutput.language, | ||||||||||||||||||||||||||||||||||
| runtime: umOutput.runtime, | ||||||||||||||||||||||||||||||||||
| framework: framework || "nextjs", | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| runConfig: { | ||||||||||||||||||||||||||||||||||
| runCommand: finalRunCommand, | ||||||||||||||||||||||||||||||||||
| environmentVariables: Object.entries(umOutput.envVars || {}) | ||||||||||||||||||||||||||||||||||
| .filter(([k]) => k !== "FIREBASE_OUTPUT_BUNDLE_DIR") | ||||||||||||||||||||||||||||||||||
| .map(([k, v]) => ({ | ||||||||||||||||||||||||||||||||||
| variable: k, | ||||||||||||||||||||||||||||||||||
| value: String(v), | ||||||||||||||||||||||||||||||||||
| availability: ["RUNTIME"], | ||||||||||||||||||||||||||||||||||
| })), | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| outputFiles: { | ||||||||||||||||||||||||||||||||||
| serverApp: { | ||||||||||||||||||||||||||||||||||
| include: [".apphosting"], | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export interface AppHostingBuildOutput { | ||||||||||||||||||||||||||||||||||
| metadata: Record<string, string | number | boolean>; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| runConfig: { | ||||||||||||||||||||||||||||||||||
| runCommand?: string; | ||||||||||||||||||||||||||||||||||
| environmentVariables?: Array<{ | ||||||||||||||||||||||||||||||||||
| variable: string; | ||||||||||||||||||||||||||||||||||
| value: string; | ||||||||||||||||||||||||||||||||||
| availability: string[]; | ||||||||||||||||||||||||||||||||||
| }>; | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| outputFiles?: { | ||||||||||||||||||||||||||||||||||
| serverApp: { | ||||||||||||||||||||||||||||||||||
| include: string[]; | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Triggers a local build of your App Hosting codebase. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * This function orchestrates the build process using the App Hosting build adapter. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * It detects the framework (though currently defaults/assumes 'nextjs' in some contexts), | ||||||||||||||||||||||||||||||||||
| * generates the necessary build artifacts, and returns metadata about the build. | ||||||||||||||||||||||||||||||||||
| * @param projectId - The project ID to use for resolving secrets. | ||||||||||||||||||||||||||||||||||
|
|
@@ -62,9 +200,26 @@ | |||||||||||||||||||||||||||||||||
| process.env[key] = value; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let apphostingBuildOutput; | ||||||||||||||||||||||||||||||||||
| let apphostingBuildOutput: AppHostingBuildOutput; | ||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| apphostingBuildOutput = await localAppHostingBuild(projectRoot, framework); | ||||||||||||||||||||||||||||||||||
| if (experiments.isEnabled("universalMaker")) { | ||||||||||||||||||||||||||||||||||
| apphostingBuildOutput = runUniversalMaker(projectRoot, framework); | ||||||||||||||||||||||||||||||||||
| logger.debug( | ||||||||||||||||||||||||||||||||||
| `[apphosting] Universal Maker build outputFiles include: ${JSON.stringify(apphostingBuildOutput.outputFiles?.serverApp?.include ?? [])}`, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| const buildResult = await localAppHostingBuild(projectRoot, framework); | ||||||||||||||||||||||||||||||||||
| apphostingBuildOutput = { | ||||||||||||||||||||||||||||||||||
| metadata: Object.fromEntries( | ||||||||||||||||||||||||||||||||||
| Object.entries(buildResult.metadata || {}).map(([k, v]) => [ | ||||||||||||||||||||||||||||||||||
| k, | ||||||||||||||||||||||||||||||||||
| v as string | number | boolean, | ||||||||||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||
| runConfig: buildResult.runConfig, | ||||||||||||||||||||||||||||||||||
| outputFiles: buildResult.outputFiles, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||
| for (const key in process.env) { | ||||||||||||||||||||||||||||||||||
| if (!(key in originalEnv)) { | ||||||||||||||||||||||||||||||||||
|
|
@@ -82,11 +237,16 @@ | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const discoveredEnv: Env[] | undefined = | ||||||||||||||||||||||||||||||||||
| apphostingBuildOutput.runConfig.environmentVariables?.map( | ||||||||||||||||||||||||||||||||||
| ({ variable, value, availability }) => ({ | ||||||||||||||||||||||||||||||||||
| variable, | ||||||||||||||||||||||||||||||||||
| value, | ||||||||||||||||||||||||||||||||||
| availability, | ||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||
| ({ variable, value, availability }) => { | ||||||||||||||||||||||||||||||||||
| const validAvail = availability.filter( | ||||||||||||||||||||||||||||||||||
| (a): a is Availability => a === "BUILD" || a === "RUNTIME", | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| variable, | ||||||||||||||||||||||||||||||||||
| value, | ||||||||||||||||||||||||||||||||||
| availability: validAvail, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.