@faable/faable 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/FaableApi.js +15 -0
- package/dist/commands/deploy/index.js +93 -54
- package/dist/commands/deploy/remote/follow.js +61 -0
- package/dist/commands/deploy/remote/index.js +48 -0
- package/dist/commands/deploy/remote/manifest.js +77 -0
- package/dist/commands/deploy/remote/upload.js +61 -0
- package/package.json +2 -1
- package/dist/commands/deploy/buildpacks/Buildpack.js +0 -11
- package/dist/commands/deploy/buildpacks/DetectError.js +0 -78
- package/dist/commands/deploy/buildpacks/docker/index.js +0 -42
- package/dist/commands/deploy/buildpacks/foreign_platforms.js +0 -21
- package/dist/commands/deploy/buildpacks/node/analyze_package.js +0 -35
- package/dist/commands/deploy/buildpacks/node/build_project.js +0 -21
- package/dist/commands/deploy/buildpacks/node/ensure_dependencies.js +0 -78
- package/dist/commands/deploy/buildpacks/node/frameworks.js +0 -108
- package/dist/commands/deploy/buildpacks/node/index.js +0 -73
- package/dist/commands/deploy/buildpacks/node/inject_serve.js +0 -20
- package/dist/commands/deploy/buildpacks/node/node_version.js +0 -41
- package/dist/commands/deploy/buildpacks/python/index.js +0 -127
- package/dist/commands/deploy/buildpacks/python/parse_procfile.js +0 -23
- package/dist/commands/deploy/buildpacks/python/providers/cerebrium.js +0 -43
- package/dist/commands/deploy/buildpacks/python/providers/parse_cerebrium_toml.js +0 -56
- package/dist/commands/deploy/buildpacks/python/providers/pipfile.js +0 -13
- package/dist/commands/deploy/buildpacks/python/providers/pyproject.js +0 -13
- package/dist/commands/deploy/buildpacks/python/providers/requirements.js +0 -14
- package/dist/commands/deploy/buildpacks/python/python_version.js +0 -41
- package/dist/commands/deploy/buildpacks/python/resolve_start.js +0 -149
- package/dist/commands/deploy/buildpacks/registry.js +0 -61
- package/dist/commands/deploy/buildpacks/shared/docker_image.js +0 -49
- package/dist/commands/deploy/buildpacks/shared/has_any_of_files.js +0 -14
- package/dist/commands/deploy/buildpacks/shared/read_text_file.js +0 -31
- package/dist/commands/deploy/buildpacks/shared/templates/Dockerfile +0 -38
- package/dist/commands/deploy/buildpacks/shared/templates/entrypoint.sh +0 -4
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import path__default from 'path';
|
|
2
|
-
import { log } from '../../../../log.js';
|
|
3
|
-
import { read_json_file } from '../shared/read_text_file.js';
|
|
4
|
-
import { detect_framework } from './frameworks.js';
|
|
5
|
-
|
|
6
|
-
const analyze_package = async (params) => {
|
|
7
|
-
const workdir = params.workdir;
|
|
8
|
-
const package_file = path__default.join(path__default.resolve(workdir), "package.json");
|
|
9
|
-
log.info(`Loading config from package.json`);
|
|
10
|
-
const pkg = read_json_file(package_file);
|
|
11
|
-
// Check if build is required to run
|
|
12
|
-
const build_script = process.env.FAABLE_NPM_BUILD_SCRIPT
|
|
13
|
-
? process.env.FAABLE_NPM_BUILD_SCRIPT
|
|
14
|
-
: pkg?.scripts?.["build"]
|
|
15
|
-
? "build"
|
|
16
|
-
: null;
|
|
17
|
-
if (!build_script) {
|
|
18
|
-
log.info(`No build script on package.json`);
|
|
19
|
-
}
|
|
20
|
-
const has_start = Boolean(pkg?.scripts?.["start"]);
|
|
21
|
-
const { type, start_command, inject_serve } = detect_framework({
|
|
22
|
-
pkg,
|
|
23
|
-
workdir,
|
|
24
|
-
has_start,
|
|
25
|
-
});
|
|
26
|
-
log.info(`⚡️ Detected deployment type=${type}`);
|
|
27
|
-
return {
|
|
28
|
-
build_script,
|
|
29
|
-
type,
|
|
30
|
-
start_command,
|
|
31
|
-
inject_serve,
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export { analyze_package };
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { log } from '../../../../log.js';
|
|
2
|
-
import { cmd } from '../../../../lib/cmd.js';
|
|
3
|
-
|
|
4
|
-
const build_project = async (args) => {
|
|
5
|
-
const { command, cwd } = args;
|
|
6
|
-
if (command) {
|
|
7
|
-
log.info(`⚙️ Building project [${command}]...`);
|
|
8
|
-
const timeout = 1000 * 60 * 30; // 30 minute timeout
|
|
9
|
-
await cmd(command, {
|
|
10
|
-
timeout,
|
|
11
|
-
cwd,
|
|
12
|
-
enableOutput: true,
|
|
13
|
-
...(args?.env ? { env: args?.env } : {}),
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
else {
|
|
17
|
-
log.info(`⚡️ No build step`);
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export { build_project };
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'fs';
|
|
2
|
-
import { join, dirname } from 'path';
|
|
3
|
-
import { log } from '../../../../log.js';
|
|
4
|
-
import { cmd } from '../../../../lib/cmd.js';
|
|
5
|
-
|
|
6
|
-
// Installing can be slow on cold CI runners (no npm cache).
|
|
7
|
-
const INSTALL_TIMEOUT = 15 * 60 * 1000; // 15 minute timeout
|
|
8
|
-
const defaultRun = (command, cwd) => cmd(command, { cwd, timeout: INSTALL_TIMEOUT, enableOutput: true });
|
|
9
|
-
const defaultHasTool = async (name) => {
|
|
10
|
-
try {
|
|
11
|
-
await cmd(`command -v ${name}`);
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* Dependencies may live in the workdir or be hoisted to a parent directory
|
|
20
|
-
* (monorepos). Walk upwards checking each level, stopping at the repo root
|
|
21
|
-
* (first directory containing `.git`) or the filesystem root.
|
|
22
|
-
*/
|
|
23
|
-
const has_node_modules = (workdir, exists) => {
|
|
24
|
-
let dir = workdir;
|
|
25
|
-
while (true) {
|
|
26
|
-
if (exists(join(dir, "node_modules")))
|
|
27
|
-
return true;
|
|
28
|
-
if (exists(join(dir, ".git")))
|
|
29
|
-
return false;
|
|
30
|
-
const parent = dirname(dir);
|
|
31
|
-
if (parent === dir)
|
|
32
|
-
return false;
|
|
33
|
-
dir = parent;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Install dependencies when `node_modules` is missing, so `faable deploy`
|
|
38
|
-
* works without a prior `npm ci` step — the generated GitHub Actions workflow
|
|
39
|
-
* is language-agnostic and no longer installs anything itself. The build
|
|
40
|
-
* (`npm run build`) and the image (`COPY . .` ships node_modules) both need
|
|
41
|
-
* dependencies present on the host.
|
|
42
|
-
*
|
|
43
|
-
* The install command honors the project's lockfile; yarn/pnpm fall back to
|
|
44
|
-
* `npm install` when the tool is missing, which may resolve slightly
|
|
45
|
-
* different versions.
|
|
46
|
-
*/
|
|
47
|
-
const ensure_dependencies = async (workdir, deps = {}) => {
|
|
48
|
-
const { run = defaultRun, hasTool = defaultHasTool, exists = existsSync } = deps;
|
|
49
|
-
if (has_node_modules(workdir, exists)) {
|
|
50
|
-
log.info(`📦 Dependencies already installed — skipping install (fresh dependencies)`);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const has = (file) => exists(join(workdir, file));
|
|
54
|
-
let install = "npm install --no-audit --no-fund";
|
|
55
|
-
if (has("package-lock.json") || has("npm-shrinkwrap.json")) {
|
|
56
|
-
install = "npm ci";
|
|
57
|
-
}
|
|
58
|
-
else if (has("yarn.lock")) {
|
|
59
|
-
if (await hasTool("yarn")) {
|
|
60
|
-
install = "yarn install --frozen-lockfile";
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
log.warn("yarn.lock found but yarn is not installed — using npm install");
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
else if (has("pnpm-lock.yaml")) {
|
|
67
|
-
if (await hasTool("pnpm")) {
|
|
68
|
-
install = "pnpm install --frozen-lockfile";
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
log.warn("pnpm-lock.yaml found but pnpm is not installed — using npm install");
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
log.info(`📦 node_modules missing — installing dependencies [${install}]`);
|
|
75
|
-
await run(install, workdir);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export { ensure_dependencies };
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import path__default from 'path';
|
|
2
|
-
import * as R from 'ramda';
|
|
3
|
-
import { log } from '../../../../log.js';
|
|
4
|
-
import { read_json_file } from '../shared/read_text_file.js';
|
|
5
|
-
|
|
6
|
-
const has_dep = (pkg, name) => Boolean(R.view(R.lensPath(["dependencies", name]), pkg) ||
|
|
7
|
-
R.view(R.lensPath(["devDependencies", name]), pkg));
|
|
8
|
-
/**
|
|
9
|
-
* Read Angular's build output path from angular.json. Defaults to `dist` when
|
|
10
|
-
* it can't be resolved. Angular ≥17 (application builder) emits into
|
|
11
|
-
* `<outputPath>/browser`, so we append it when the project uses that builder.
|
|
12
|
-
*/
|
|
13
|
-
const resolve_angular_output = (workdir) => {
|
|
14
|
-
const fallback = "dist";
|
|
15
|
-
try {
|
|
16
|
-
const angular_json = read_json_file(path__default.join(workdir, "angular.json"));
|
|
17
|
-
const projects = angular_json?.projects ?? {};
|
|
18
|
-
const project_name = angular_json?.defaultProject ?? Object.keys(projects)[0];
|
|
19
|
-
const build = projects?.[project_name]?.architect?.build;
|
|
20
|
-
const output = build?.options?.outputPath;
|
|
21
|
-
if (!output)
|
|
22
|
-
return fallback;
|
|
23
|
-
const builder = build?.builder ?? "";
|
|
24
|
-
const is_application_builder = builder.includes("application") || builder.includes("browser-esbuild");
|
|
25
|
-
return is_application_builder ? path__default.join(output, "browser") : output;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return fallback;
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Framework registry, evaluated in order. Order matters: Astro/SvelteKit/CRA
|
|
33
|
-
* pull Vite in transitively, so Vite must be the last static fallback.
|
|
34
|
-
*/
|
|
35
|
-
const FRAMEWORKS = [
|
|
36
|
-
// Next.js: handled by its own runtime_strategy/PVC, never static-served here.
|
|
37
|
-
{ type: "next", deps: ["next"] },
|
|
38
|
-
{
|
|
39
|
-
type: "astro",
|
|
40
|
-
deps: ["astro"],
|
|
41
|
-
outputDir: "dist",
|
|
42
|
-
serveCommand: (_dir) => `npx astro preview --host 0.0.0.0 --port $PORT`,
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
type: "gatsby",
|
|
46
|
-
deps: ["gatsby"],
|
|
47
|
-
outputDir: "public",
|
|
48
|
-
serveCommand: (_dir) => `npx gatsby serve --host 0.0.0.0 --port $PORT`,
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
type: "cra",
|
|
52
|
-
deps: ["react-scripts"],
|
|
53
|
-
outputDir: "build",
|
|
54
|
-
injectServe: true,
|
|
55
|
-
serveCommand: (dir) => `npx serve -s ${dir} -l $PORT`,
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
type: "vue",
|
|
59
|
-
deps: ["@vue/cli-service"],
|
|
60
|
-
outputDir: "dist",
|
|
61
|
-
injectServe: true,
|
|
62
|
-
serveCommand: (dir) => `npx serve -s ${dir} -l $PORT`,
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
type: "angular",
|
|
66
|
-
deps: ["@angular/cli", "@angular-devkit/build-angular"],
|
|
67
|
-
injectServe: true,
|
|
68
|
-
resolveOutput: resolve_angular_output,
|
|
69
|
-
serveCommand: (dir) => `npx serve -s ${dir} -l $PORT`,
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
type: "vite",
|
|
73
|
-
deps: ["vite"],
|
|
74
|
-
outputDir: "dist",
|
|
75
|
-
serveCommand: (_dir) => `npx vite preview --host 0.0.0.0 --port $PORT`,
|
|
76
|
-
},
|
|
77
|
-
];
|
|
78
|
-
/**
|
|
79
|
-
* Detect the framework from package.json and compute how to serve it.
|
|
80
|
-
*
|
|
81
|
-
* When the project defines its own `start` script we never override it (the app
|
|
82
|
-
* ships a real server — custom SSR, Nuxt, Remix, SvelteKit node-adapter, etc.),
|
|
83
|
-
* so `start_command`/`inject_serve` stay neutral.
|
|
84
|
-
*/
|
|
85
|
-
const detect_framework = (params) => {
|
|
86
|
-
const { pkg, workdir, has_start } = params;
|
|
87
|
-
const framework = FRAMEWORKS.find((fw) => fw.deps.some((dep) => has_dep(pkg, dep)));
|
|
88
|
-
if (!framework) {
|
|
89
|
-
return { type: "node", start_command: null, inject_serve: false };
|
|
90
|
-
}
|
|
91
|
-
// Static frameworks only override the start command when the project doesn't
|
|
92
|
-
// ship its own server.
|
|
93
|
-
if (framework.serveCommand && !has_start) {
|
|
94
|
-
const output_dir = framework.resolveOutput
|
|
95
|
-
? framework.resolveOutput(workdir)
|
|
96
|
-
: framework.outputDir ?? "dist";
|
|
97
|
-
const start_command = framework.serveCommand(output_dir);
|
|
98
|
-
log.info(`No start script on package.json, serving ${framework.type} output (${output_dir}) with [${start_command}]`);
|
|
99
|
-
return {
|
|
100
|
-
type: framework.type,
|
|
101
|
-
start_command,
|
|
102
|
-
inject_serve: Boolean(framework.injectServe),
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
return { type: framework.type, start_command: null, inject_serve: false };
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
export { FRAMEWORKS, detect_framework, resolve_angular_output };
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import * as R from 'ramda';
|
|
2
|
-
import { log } from '../../../../log.js';
|
|
3
|
-
import { render_dockerfile, build_image } from '../shared/docker_image.js';
|
|
4
|
-
import { has_any_of_files } from '../shared/has_any_of_files.js';
|
|
5
|
-
import { analyze_package } from './analyze_package.js';
|
|
6
|
-
import { build_project } from './build_project.js';
|
|
7
|
-
import { ensure_dependencies } from './ensure_dependencies.js';
|
|
8
|
-
import { inject_serve } from './inject_serve.js';
|
|
9
|
-
import { resolve_node_version } from './node_version.js';
|
|
10
|
-
|
|
11
|
-
const BANNER = `NODE_VERSION=$(node --version)
|
|
12
|
-
NPM_VERSION=$(npm --version)
|
|
13
|
-
YARN_VERSION=$(yarn --version 2>/dev/null || echo "n/a")
|
|
14
|
-
|
|
15
|
-
echo "Faable Cloud · [node $NODE_VERSION] [npm $NPM_VERSION] [yarn $YARN_VERSION]"`;
|
|
16
|
-
const node_buildpack = {
|
|
17
|
-
name: "node",
|
|
18
|
-
detect_files: ["package.json"],
|
|
19
|
-
async detect(ctx) {
|
|
20
|
-
if (!has_any_of_files(this.detect_files, ctx.workdir))
|
|
21
|
-
return null;
|
|
22
|
-
const version = await resolve_node_version(ctx.workdir);
|
|
23
|
-
const { build_script, type, start_command, inject_serve } = await analyze_package({ workdir: ctx.workdir });
|
|
24
|
-
// Precedence: explicit faable.json startCommand > framework-detected
|
|
25
|
-
// command (e.g. serving a static SPA) > default `npm run start`.
|
|
26
|
-
const start = ctx.config.startCommand ?? start_command ?? "npm run start";
|
|
27
|
-
return {
|
|
28
|
-
buildpack: "node",
|
|
29
|
-
runtime: { name: "node", version },
|
|
30
|
-
type,
|
|
31
|
-
start_command: start,
|
|
32
|
-
build_script,
|
|
33
|
-
inject_serve,
|
|
34
|
-
from: `node:${version}`,
|
|
35
|
-
};
|
|
36
|
-
},
|
|
37
|
-
async build(ctx, plan) {
|
|
38
|
-
const env = {
|
|
39
|
-
...R.fromPairs(ctx.env_vars.map((e) => [e.name, e.value])),
|
|
40
|
-
// Platform-injected build-time identity, mirroring the runtime env the
|
|
41
|
-
// controller sets on the pod. FAABLE_DEPLOY_ID (build id ≡ runtime id)
|
|
42
|
-
// is the deterministic Next.js buildId source — user secrets can't
|
|
43
|
-
// override either. See arch/deploy/version-skew-coexistence.md.
|
|
44
|
-
FAABLE_APP_ID: ctx.app.id,
|
|
45
|
-
FAABLE_DEPLOY_ID: ctx.deployment.id,
|
|
46
|
-
};
|
|
47
|
-
log.info(`Building with env variables ${Object.keys(env).join(",")}`);
|
|
48
|
-
// The workflow no longer runs `npm ci` — install here when needed, before
|
|
49
|
-
// the build and before `COPY . .` packages node_modules into the image.
|
|
50
|
-
await ensure_dependencies(ctx.workdir);
|
|
51
|
-
// Host build step: npm script from the plan, else faable.json buildCommand.
|
|
52
|
-
const build_command = plan.build_script
|
|
53
|
-
? `npm run ${plan.build_script}`
|
|
54
|
-
: ctx.config.buildCommand;
|
|
55
|
-
await build_project({ command: build_command, env, cwd: ctx.workdir });
|
|
56
|
-
// Frameworks without a bundled static server (CRA/Vue/Angular) need
|
|
57
|
-
// `serve` installed into node_modules before packaging.
|
|
58
|
-
if (plan.inject_serve) {
|
|
59
|
-
await inject_serve(ctx.workdir);
|
|
60
|
-
}
|
|
61
|
-
log.info(`⚙️ Start command: ${plan.start_command}`);
|
|
62
|
-
log.info(`Using docker image ${plan.from}-slim`);
|
|
63
|
-
const dockerfile = render_dockerfile({
|
|
64
|
-
from: plan.from,
|
|
65
|
-
env: { NODE_ENV: "production" },
|
|
66
|
-
banner: BANNER,
|
|
67
|
-
start_command: plan.start_command,
|
|
68
|
-
});
|
|
69
|
-
await build_image({ app: ctx.app, workdir: ctx.workdir, dockerfile });
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export { node_buildpack };
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { log } from '../../../../log.js';
|
|
2
|
-
import { cmd } from '../../../../lib/cmd.js';
|
|
3
|
-
|
|
4
|
-
// Pinned for reproducible builds. `serve` is the standalone static server used
|
|
5
|
-
// for frameworks without a bundled preview tool (CRA, Vue, Angular).
|
|
6
|
-
const SERVE_VERSION = "14";
|
|
7
|
-
/**
|
|
8
|
-
* Install `serve` into the project's node_modules so it ships inside the image
|
|
9
|
-
* via `COPY . .` (the Dockerfile does no `npm install`). This lets `npx serve`
|
|
10
|
-
* resolve the local copy at container start — no runtime download needed.
|
|
11
|
-
*
|
|
12
|
-
* `--no-save` keeps the user's package.json/lockfile untouched.
|
|
13
|
-
*/
|
|
14
|
-
const inject_serve = async (workdir) => {
|
|
15
|
-
log.info(`📥 Injecting static server (serve@${SERVE_VERSION}) into image`);
|
|
16
|
-
const timeout = 5 * 60 * 1000; // 5 minute timeout
|
|
17
|
-
await cmd(`npm install serve@${SERVE_VERSION} --no-save --no-audit --no-fund`, { cwd: workdir, timeout, enableOutput: true });
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export { inject_serve };
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import path__default from 'path';
|
|
2
|
-
import { cmd } from '../../../../lib/cmd.js';
|
|
3
|
-
import { log } from '../../../../log.js';
|
|
4
|
-
import { read_json_file } from '../shared/read_text_file.js';
|
|
5
|
-
|
|
6
|
-
const getCurrentNodeVersion = async () => {
|
|
7
|
-
const out = await cmd(`node --version`);
|
|
8
|
-
const [_, version] = out.stdout.toString().trim().split("v");
|
|
9
|
-
return version;
|
|
10
|
-
};
|
|
11
|
-
/**
|
|
12
|
-
* Resolve the Node version for the base image: `engines.node` from
|
|
13
|
-
* package.json (resolved to a concrete release via `npm view`), else the
|
|
14
|
-
* version running the deploy. Also validates the package has a `name`
|
|
15
|
-
* (required since the beginning; kept for compatibility).
|
|
16
|
-
*/
|
|
17
|
-
const resolve_node_version = async (workdir) => {
|
|
18
|
-
const packageJSONFile = path__default.join(workdir, "package.json");
|
|
19
|
-
const { name, engines } = read_json_file(packageJSONFile);
|
|
20
|
-
if (!name) {
|
|
21
|
-
throw new Error("Missing name in package.json");
|
|
22
|
-
}
|
|
23
|
-
let runtime_version = await getCurrentNodeVersion();
|
|
24
|
-
if (engines?.node) {
|
|
25
|
-
try {
|
|
26
|
-
const check_cmd = `npm view node@"${engines.node}" version | tail -n 1 | cut -d "'" -f2`;
|
|
27
|
-
const out = await cmd(check_cmd);
|
|
28
|
-
runtime_version = out.stdout.toString().trim();
|
|
29
|
-
log.info(`Using node@${runtime_version} from engines in package.json (${engines.node})`);
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
log.info(`Node version defined in engines in package.json is not valid (${engines.node}), using current version ${runtime_version}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
log.info(`Node version ${runtime_version}`);
|
|
37
|
-
}
|
|
38
|
-
return runtime_version;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export { resolve_node_version };
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path__default from 'path';
|
|
3
|
-
import { log } from '../../../../log.js';
|
|
4
|
-
import { render_dockerfile, build_image } from '../shared/docker_image.js';
|
|
5
|
-
import { has_any_of_files } from '../shared/has_any_of_files.js';
|
|
6
|
-
import { read_text_file } from '../shared/read_text_file.js';
|
|
7
|
-
import { cerebrium_provider } from './providers/cerebrium.js';
|
|
8
|
-
import { pipfile_provider } from './providers/pipfile.js';
|
|
9
|
-
import { pyproject_provider } from './providers/pyproject.js';
|
|
10
|
-
import { requirements_provider } from './providers/requirements.js';
|
|
11
|
-
import { resolve_python_version } from './python_version.js';
|
|
12
|
-
import { resolve_start, with_server_injection, read_dependencies_text } from './resolve_start.js';
|
|
13
|
-
|
|
14
|
-
const BANNER = `PYTHON_VERSION=$(python --version 2>&1)
|
|
15
|
-
PIP_VERSION=$(pip --version 2>&1)
|
|
16
|
-
|
|
17
|
-
echo "Faable Cloud · [$PYTHON_VERSION] [$PIP_VERSION]"`;
|
|
18
|
-
const DOCS_URL = "https://faable.com/docs/deploy/build-requirements";
|
|
19
|
-
// Evaluated in order: the first provider whose manifest exists supplies the
|
|
20
|
-
// install command. Classic manifests come before platform-specific ones, so
|
|
21
|
-
// a repo shipping both requirements.txt and cerebrium.toml builds from the
|
|
22
|
-
// standard manifest.
|
|
23
|
-
const PROVIDERS = [
|
|
24
|
-
requirements_provider,
|
|
25
|
-
pyproject_provider,
|
|
26
|
-
pipfile_provider,
|
|
27
|
-
cerebrium_provider,
|
|
28
|
-
];
|
|
29
|
-
const detect_with_provider = (ctx, provider) => {
|
|
30
|
-
const resolved = provider.resolve(ctx.workdir);
|
|
31
|
-
// Framework detection blob: all classic manifests plus whatever the winning
|
|
32
|
-
// provider contributes (e.g. cerebrium pip package names).
|
|
33
|
-
const deps = [read_dependencies_text(ctx.workdir), resolved.deps_text ?? ""]
|
|
34
|
-
.join("\n")
|
|
35
|
-
.toLowerCase();
|
|
36
|
-
let start_command;
|
|
37
|
-
let server;
|
|
38
|
-
try {
|
|
39
|
-
({ start_command, server } = resolve_start(ctx.workdir, deps, ctx.config, resolved.start_hint));
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
if (provider.name === "cerebrium") {
|
|
43
|
-
throw new Error("Detected a Cerebrium project (cerebrium.toml) but couldn't find a web " +
|
|
44
|
-
"entrypoint. Faable runs web services — expose a FastAPI/Flask app or " +
|
|
45
|
-
`set \`startCommand\` in faable.json. Docs: ${DOCS_URL}`, { cause: error });
|
|
46
|
-
}
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
// faable.json buildCommand overrides the provider's install. The cacheable
|
|
50
|
-
// manifest layer only applies to the provider's own command — an arbitrary
|
|
51
|
-
// buildCommand may need the full source.
|
|
52
|
-
const base_install = ctx.config.buildCommand ?? resolved.install_command;
|
|
53
|
-
const install_command = with_server_injection(base_install, server, deps);
|
|
54
|
-
const install_files = ctx.config.buildCommand
|
|
55
|
-
? undefined
|
|
56
|
-
: resolved.install_files;
|
|
57
|
-
const version = resolve_python_version(ctx.workdir, resolved.python_version);
|
|
58
|
-
log.info(`Using python@${version}`);
|
|
59
|
-
log.info(`📦 Install command: ${install_command}`);
|
|
60
|
-
log.info(`⚙️ Start command: ${start_command}`);
|
|
61
|
-
return {
|
|
62
|
-
buildpack: "python",
|
|
63
|
-
runtime: { name: "python", version },
|
|
64
|
-
type: "python",
|
|
65
|
-
start_command,
|
|
66
|
-
install_command,
|
|
67
|
-
install_files,
|
|
68
|
-
from: `python:${version}`,
|
|
69
|
-
};
|
|
70
|
-
};
|
|
71
|
-
const python_buildpack = {
|
|
72
|
-
name: "python",
|
|
73
|
-
detect_files: PROVIDERS.flatMap((p) => p.files),
|
|
74
|
-
fallback_files: ["main.py", "app.py", "wsgi.py"],
|
|
75
|
-
async detect(ctx) {
|
|
76
|
-
const provider = PROVIDERS.find((p) => has_any_of_files(p.files, ctx.workdir));
|
|
77
|
-
if (!provider)
|
|
78
|
-
return null;
|
|
79
|
-
return detect_with_provider(ctx, provider);
|
|
80
|
-
},
|
|
81
|
-
// Weak-signal pass: a Python entrypoint with no dependency manifest at all.
|
|
82
|
-
// Registered as fallback so it loses against any strong trigger (Dockerfile).
|
|
83
|
-
async detect_fallback(ctx) {
|
|
84
|
-
const entries = this.fallback_files.filter((f) => fs.existsSync(path__default.join(ctx.workdir, f)));
|
|
85
|
-
if (entries.length === 0)
|
|
86
|
-
return null;
|
|
87
|
-
log.warn(`⚠️ No dependency manifest (requirements.txt/pyproject.toml/Pipfile) found — ` +
|
|
88
|
-
`building from ${entries.join(", ")} without installing dependencies. ` +
|
|
89
|
-
`Your app will likely need a requirements.txt; see ${DOCS_URL}`);
|
|
90
|
-
// Framework detection blob = the entry files themselves: an
|
|
91
|
-
// `from fastapi import FastAPI` line carries the token detection needs.
|
|
92
|
-
const deps = entries
|
|
93
|
-
.map((f) => read_text_file(path__default.join(ctx.workdir, f)))
|
|
94
|
-
.join("\n")
|
|
95
|
-
.toLowerCase();
|
|
96
|
-
const { start_command, server } = resolve_start(ctx.workdir, deps, ctx.config);
|
|
97
|
-
const install_command = with_server_injection(ctx.config.buildCommand, server, deps);
|
|
98
|
-
const version = resolve_python_version(ctx.workdir);
|
|
99
|
-
log.info(`Using python@${version}`);
|
|
100
|
-
log.info(`📦 Install command: ${install_command}`);
|
|
101
|
-
log.info(`⚙️ Start command: ${start_command}`);
|
|
102
|
-
return {
|
|
103
|
-
buildpack: "python",
|
|
104
|
-
runtime: { name: "python", version },
|
|
105
|
-
type: "python",
|
|
106
|
-
start_command,
|
|
107
|
-
install_command,
|
|
108
|
-
from: `python:${version}`,
|
|
109
|
-
};
|
|
110
|
-
},
|
|
111
|
-
async build(ctx, plan) {
|
|
112
|
-
// Dependencies install inside the image (unlike node); nothing runs on
|
|
113
|
-
// the host here.
|
|
114
|
-
log.info(`Using docker image ${plan.from}-slim`);
|
|
115
|
-
const dockerfile = render_dockerfile({
|
|
116
|
-
from: plan.from,
|
|
117
|
-
env: { PYTHONUNBUFFERED: "1" },
|
|
118
|
-
banner: BANNER,
|
|
119
|
-
start_command: plan.start_command,
|
|
120
|
-
install_command: plan.install_command,
|
|
121
|
-
install_files: plan.install_files,
|
|
122
|
-
});
|
|
123
|
-
await build_image({ app: ctx.app, workdir: ctx.workdir, dockerfile });
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
export { python_buildpack };
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path__default from 'path';
|
|
3
|
-
import { read_text_file } from '../shared/read_text_file.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Read the `web:` process command from a Procfile, if present. Returns null when
|
|
7
|
-
* there's no Procfile or no `web` entry. Used as a manual start-command override
|
|
8
|
-
* (Heroku-style) before falling back to framework detection.
|
|
9
|
-
*/
|
|
10
|
-
const parse_procfile = (workdir) => {
|
|
11
|
-
const procfile = path__default.join(workdir, "Procfile");
|
|
12
|
-
if (!fs.existsSync(procfile))
|
|
13
|
-
return null;
|
|
14
|
-
const content = read_text_file(procfile);
|
|
15
|
-
for (const line of content.split("\n")) {
|
|
16
|
-
const match = line.match(/^\s*web\s*:\s*(.+?)\s*$/);
|
|
17
|
-
if (match?.[1])
|
|
18
|
-
return match[1];
|
|
19
|
-
}
|
|
20
|
-
return null;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export { parse_procfile };
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import path__default from 'path';
|
|
2
|
-
import { read_text_file } from '../../shared/read_text_file.js';
|
|
3
|
-
import { parse_cerebrium_toml } from './parse_cerebrium_toml.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Cerebrium projects (cerebrium.toml + a Python entrypoint) declare their pip
|
|
7
|
-
* dependencies and python version in the toml instead of a requirements.txt.
|
|
8
|
-
* This provider translates that manifest so the standard python buildpack can
|
|
9
|
-
* build them — the masyosh/cere onboarding case.
|
|
10
|
-
*/
|
|
11
|
-
const cerebrium_provider = {
|
|
12
|
-
name: "cerebrium",
|
|
13
|
-
files: ["cerebrium.toml"],
|
|
14
|
-
resolve(workdir) {
|
|
15
|
-
const manifest = parse_cerebrium_toml(read_text_file(path__default.join(workdir, "cerebrium.toml")));
|
|
16
|
-
let install_command;
|
|
17
|
-
let install_files;
|
|
18
|
-
if (manifest.pip_requirements_file) {
|
|
19
|
-
install_command = `pip install --no-cache-dir -r ${manifest.pip_requirements_file}`;
|
|
20
|
-
// Copying the referenced file AND the toml ties the cached layer to both.
|
|
21
|
-
install_files = [manifest.pip_requirements_file, "cerebrium.toml"];
|
|
22
|
-
}
|
|
23
|
-
else if (manifest.pip_packages.length > 0) {
|
|
24
|
-
install_command = `pip install --no-cache-dir ${manifest.pip_packages
|
|
25
|
-
.map((spec) => `"${spec}"`)
|
|
26
|
-
.join(" ")}`;
|
|
27
|
-
// The command inlines the packages; copying the toml keeps the layer
|
|
28
|
-
// cache keyed to the manifest content.
|
|
29
|
-
install_files = ["cerebrium.toml"];
|
|
30
|
-
}
|
|
31
|
-
return {
|
|
32
|
-
install_command,
|
|
33
|
-
install_files,
|
|
34
|
-
python_version: manifest.python_version,
|
|
35
|
-
// Feed the pip package names into the framework-detection blob so e.g.
|
|
36
|
-
// `fastapi` in the toml triggers the existing uvicorn start resolution.
|
|
37
|
-
deps_text: manifest.pip_names.join("\n"),
|
|
38
|
-
start_hint: manifest.entrypoint,
|
|
39
|
-
};
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export { cerebrium_provider };
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { parse } from 'smol-toml';
|
|
2
|
-
import { log } from '../../../../../log.js';
|
|
3
|
-
|
|
4
|
-
const table = (value) => value && typeof value === "object" && !Array.isArray(value)
|
|
5
|
-
? value
|
|
6
|
-
: {};
|
|
7
|
-
/**
|
|
8
|
-
* Turn the pip table into installable requirement specs. Cerebrium uses
|
|
9
|
-
* `"latest"` / `""` for unpinned packages; anything else is a version spec —
|
|
10
|
-
* exact pins (`"2.0.0"`) become `==`, ranges (`">=2.0"`) pass through.
|
|
11
|
-
*/
|
|
12
|
-
const to_spec = (name, version) => {
|
|
13
|
-
const v = String(version ?? "").trim();
|
|
14
|
-
if (!v || v.toLowerCase() === "latest")
|
|
15
|
-
return name;
|
|
16
|
-
if (/^[0-9]/.test(v))
|
|
17
|
-
return `${name}==${v}`;
|
|
18
|
-
return `${name}${v}`;
|
|
19
|
-
};
|
|
20
|
-
const parse_cerebrium_toml = (content) => {
|
|
21
|
-
const root = table(table(parse(content)).cerebrium);
|
|
22
|
-
const deployment = table(root.deployment);
|
|
23
|
-
const dependencies = table(root.dependencies);
|
|
24
|
-
const pip = table(dependencies.pip);
|
|
25
|
-
const paths = table(dependencies.paths);
|
|
26
|
-
const custom = table(table(root.runtime).custom);
|
|
27
|
-
for (const skipped of ["apt", "conda"]) {
|
|
28
|
-
if (Object.keys(table(dependencies[skipped])).length > 0) {
|
|
29
|
-
log.warn(`cerebrium.toml declares ${skipped} dependencies — Faable only installs the pip table, ${skipped} packages are ignored`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
const pip_names = Object.keys(pip);
|
|
33
|
-
const pip_packages = pip_names.map((name) => to_spec(name, pip[name]));
|
|
34
|
-
const entrypoint_raw = custom.entrypoint;
|
|
35
|
-
const entrypoint = Array.isArray(entrypoint_raw)
|
|
36
|
-
? entrypoint_raw.map(String).join(" ")
|
|
37
|
-
: typeof entrypoint_raw === "string" && entrypoint_raw.trim()
|
|
38
|
-
? entrypoint_raw.trim()
|
|
39
|
-
: undefined;
|
|
40
|
-
const python_version = typeof deployment.python_version === "string" &&
|
|
41
|
-
deployment.python_version.trim()
|
|
42
|
-
? deployment.python_version.trim()
|
|
43
|
-
: undefined;
|
|
44
|
-
const pip_requirements_file = typeof paths.pip === "string" && paths.pip.trim()
|
|
45
|
-
? paths.pip.trim()
|
|
46
|
-
: undefined;
|
|
47
|
-
return {
|
|
48
|
-
python_version,
|
|
49
|
-
pip_packages,
|
|
50
|
-
pip_names,
|
|
51
|
-
pip_requirements_file,
|
|
52
|
-
entrypoint,
|
|
53
|
-
};
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export { parse_cerebrium_toml };
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
const pipfile_provider = {
|
|
2
|
-
name: "pipfile",
|
|
3
|
-
files: ["Pipfile"],
|
|
4
|
-
resolve() {
|
|
5
|
-
return {
|
|
6
|
-
// `pipenv install --deploy` reads Pipfile.lock too — keep the install
|
|
7
|
-
// after `COPY . .` so it sees whatever the repo ships.
|
|
8
|
-
install_command: "pip install --no-cache-dir pipenv && pipenv install --system --deploy",
|
|
9
|
-
};
|
|
10
|
-
},
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export { pipfile_provider };
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
const pyproject_provider = {
|
|
2
|
-
name: "pyproject",
|
|
3
|
-
files: ["pyproject.toml"],
|
|
4
|
-
resolve() {
|
|
5
|
-
return {
|
|
6
|
-
// `pip install .` builds the package, so it needs the full source —
|
|
7
|
-
// no install_files: the install runs after `COPY . .`.
|
|
8
|
-
install_command: "pip install --no-cache-dir .",
|
|
9
|
-
};
|
|
10
|
-
},
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export { pyproject_provider };
|