@faable/faable 1.17.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 -83
- 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/node/wrap_next_config.js +0 -138
- 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,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 };
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const requirements_provider = {
|
|
2
|
-
name: "requirements",
|
|
3
|
-
files: ["requirements.txt"],
|
|
4
|
-
resolve() {
|
|
5
|
-
return {
|
|
6
|
-
install_command: "pip install --no-cache-dir -r requirements.txt",
|
|
7
|
-
// Copying only the manifest keeps the install layer cached until
|
|
8
|
-
// requirements.txt itself changes.
|
|
9
|
-
install_files: ["requirements.txt"],
|
|
10
|
-
};
|
|
11
|
-
},
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export { requirements_provider };
|
|
@@ -1,41 +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
|
-
const DEFAULT_VERSION = "3.11.3";
|
|
6
|
-
/**
|
|
7
|
-
* Resolve the Python version from (in order):
|
|
8
|
-
* 1. runtime.txt → `python-3.11.3`
|
|
9
|
-
* 2. .python-version → `3.11.3` (pyenv)
|
|
10
|
-
* 3. provider hint → e.g. cerebrium.toml `python_version`
|
|
11
|
-
* 4. pyproject.toml → `requires-python = ">=3.11"` (first concrete X.Y[.Z])
|
|
12
|
-
* Falls back to DEFAULT_VERSION.
|
|
13
|
-
*/
|
|
14
|
-
const resolve_python_version = (workdir, provider_hint) => {
|
|
15
|
-
const runtime_config = path__default.join(workdir, "runtime.txt");
|
|
16
|
-
if (fs.existsSync(runtime_config)) {
|
|
17
|
-
const runtime_data = read_text_file(runtime_config).trim();
|
|
18
|
-
if (!runtime_data.startsWith("python-")) {
|
|
19
|
-
throw new Error("runtime.txt must have runtime format with python-<version>");
|
|
20
|
-
}
|
|
21
|
-
return runtime_data.split("-")[1];
|
|
22
|
-
}
|
|
23
|
-
const python_version_file = path__default.join(workdir, ".python-version");
|
|
24
|
-
if (fs.existsSync(python_version_file)) {
|
|
25
|
-
const version = read_text_file(python_version_file).trim();
|
|
26
|
-
if (version)
|
|
27
|
-
return version;
|
|
28
|
-
}
|
|
29
|
-
if (provider_hint)
|
|
30
|
-
return provider_hint;
|
|
31
|
-
const pyproject = path__default.join(workdir, "pyproject.toml");
|
|
32
|
-
if (fs.existsSync(pyproject)) {
|
|
33
|
-
const toml = read_text_file(pyproject);
|
|
34
|
-
const match = toml.match(/requires-python\s*=\s*["'][^0-9]*([0-9]+\.[0-9]+(?:\.[0-9]+)?)/);
|
|
35
|
-
if (match?.[1])
|
|
36
|
-
return match[1];
|
|
37
|
-
}
|
|
38
|
-
return DEFAULT_VERSION;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export { resolve_python_version };
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path__default from 'path';
|
|
3
|
-
import { log } from '../../../../log.js';
|
|
4
|
-
import { read_text_file } from '../shared/read_text_file.js';
|
|
5
|
-
import { parse_procfile } from './parse_procfile.js';
|
|
6
|
-
|
|
7
|
-
/** Combine all classic dependency manifests into one lowercased blob. */
|
|
8
|
-
const read_dependencies_text = (workdir) => {
|
|
9
|
-
const files = ["requirements.txt", "pyproject.toml", "Pipfile"];
|
|
10
|
-
return files
|
|
11
|
-
.map((f) => path__default.join(workdir, f))
|
|
12
|
-
.filter((p) => fs.existsSync(p))
|
|
13
|
-
.map((p) => read_text_file(p))
|
|
14
|
-
.join("\n")
|
|
15
|
-
.toLowerCase();
|
|
16
|
-
};
|
|
17
|
-
const has_token = (deps, token) => new RegExp(`\\b${token}\\b`).test(deps);
|
|
18
|
-
/** Identify which server binary a start command relies on, to ensure it's installed. */
|
|
19
|
-
const server_from_command = (command) => {
|
|
20
|
-
const bin = command.trim().split(/\s+/)[0];
|
|
21
|
-
if (bin === "gunicorn")
|
|
22
|
-
return "gunicorn";
|
|
23
|
-
if (bin === "uvicorn")
|
|
24
|
-
return "uvicorn";
|
|
25
|
-
return null;
|
|
26
|
-
};
|
|
27
|
-
/** Django project package = the directory that contains wsgi.py. */
|
|
28
|
-
const find_django_package = (workdir) => {
|
|
29
|
-
const entries = fs.readdirSync(workdir, { withFileTypes: true });
|
|
30
|
-
for (const entry of entries) {
|
|
31
|
-
if (entry.isDirectory() &&
|
|
32
|
-
fs.existsSync(path__default.join(workdir, entry.name, "wsgi.py"))) {
|
|
33
|
-
return entry.name;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* Find the module that defines the app object, returning its dotted module path
|
|
40
|
-
* (e.g. `main`, `app.main`). Prefers a file matching `pattern`, else the first
|
|
41
|
-
* existing candidate.
|
|
42
|
-
*/
|
|
43
|
-
const find_app_module = (workdir, pattern) => {
|
|
44
|
-
const candidates = [
|
|
45
|
-
"main.py",
|
|
46
|
-
"app.py",
|
|
47
|
-
"asgi.py",
|
|
48
|
-
"wsgi.py",
|
|
49
|
-
"application.py",
|
|
50
|
-
"server.py",
|
|
51
|
-
path__default.join("app", "main.py"),
|
|
52
|
-
path__default.join("app", "app.py"),
|
|
53
|
-
path__default.join("src", "main.py"),
|
|
54
|
-
];
|
|
55
|
-
let first_existing = null;
|
|
56
|
-
for (const rel of candidates) {
|
|
57
|
-
const abs = path__default.join(workdir, rel);
|
|
58
|
-
if (!fs.existsSync(abs))
|
|
59
|
-
continue;
|
|
60
|
-
const module = rel.replace(/\.py$/, "").split(path__default.sep).join(".");
|
|
61
|
-
if (!first_existing)
|
|
62
|
-
first_existing = module;
|
|
63
|
-
if (pattern.test(read_text_file(abs)))
|
|
64
|
-
return module;
|
|
65
|
-
}
|
|
66
|
-
return first_existing;
|
|
67
|
-
};
|
|
68
|
-
/**
|
|
69
|
-
* Resolve the container start command and which server it needs installed.
|
|
70
|
-
* Precedence: faable.json startCommand → Procfile `web:` → provider start
|
|
71
|
-
* hint (e.g. cerebrium entrypoint) → framework detection (Django → FastAPI/
|
|
72
|
-
* ASGI → Flask).
|
|
73
|
-
*/
|
|
74
|
-
const resolve_start = (workdir, deps, config, start_hint) => {
|
|
75
|
-
// 1. Explicit override in faable.json
|
|
76
|
-
if (config.startCommand) {
|
|
77
|
-
log.info(`Using start command from faable.json`);
|
|
78
|
-
return {
|
|
79
|
-
start_command: config.startCommand,
|
|
80
|
-
server: server_from_command(config.startCommand),
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
// 2. Procfile `web:` line
|
|
84
|
-
const procfile = parse_procfile(workdir);
|
|
85
|
-
if (procfile) {
|
|
86
|
-
log.info(`Using start command from Procfile`);
|
|
87
|
-
return { start_command: procfile, server: server_from_command(procfile) };
|
|
88
|
-
}
|
|
89
|
-
// 3. Provider hint (e.g. cerebrium.toml runtime entrypoint)
|
|
90
|
-
if (start_hint) {
|
|
91
|
-
log.info(`Using start command from the project manifest`);
|
|
92
|
-
return { start_command: start_hint, server: server_from_command(start_hint) };
|
|
93
|
-
}
|
|
94
|
-
// 4. Framework detection
|
|
95
|
-
// Django: manage.py + the package holding wsgi.py
|
|
96
|
-
if (fs.existsSync(path__default.join(workdir, "manage.py"))) {
|
|
97
|
-
const pkg = find_django_package(workdir);
|
|
98
|
-
if (!pkg) {
|
|
99
|
-
throw new Error("Detected Django (manage.py) but couldn't find the wsgi.py package. Set `startCommand` in faable.json or add a Procfile.");
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
start_command: `gunicorn ${pkg}.wsgi:application --bind 0.0.0.0:$PORT`,
|
|
103
|
-
server: "gunicorn",
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
// FastAPI / ASGI
|
|
107
|
-
if (has_token(deps, "fastapi") ||
|
|
108
|
-
has_token(deps, "starlette") ||
|
|
109
|
-
has_token(deps, "uvicorn")) {
|
|
110
|
-
const module = find_app_module(workdir, /app\s*=\s*(FastAPI|Starlette)\(/i);
|
|
111
|
-
if (module) {
|
|
112
|
-
return {
|
|
113
|
-
start_command: `uvicorn ${module}:app --host 0.0.0.0 --port $PORT`,
|
|
114
|
-
server: "uvicorn",
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
// Flask
|
|
119
|
-
if (has_token(deps, "flask")) {
|
|
120
|
-
const module = find_app_module(workdir, /app\s*=\s*Flask\(/i);
|
|
121
|
-
if (module) {
|
|
122
|
-
return {
|
|
123
|
-
start_command: `gunicorn ${module}:app --bind 0.0.0.0:$PORT`,
|
|
124
|
-
server: "gunicorn",
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
throw new Error("Could not detect how to start this Python app. Set `startCommand` in faable.json or add a Procfile with a `web:` line.");
|
|
129
|
-
};
|
|
130
|
-
/**
|
|
131
|
-
* Append the WSGI/ASGI server install when the start command needs one that
|
|
132
|
-
* isn't already declared in the dependencies. Returns "true" (no-op) when
|
|
133
|
-
* there is nothing to install at all.
|
|
134
|
-
*/
|
|
135
|
-
const with_server_injection = (base, server, deps) => {
|
|
136
|
-
const steps = base ? [base] : [];
|
|
137
|
-
if (server === "gunicorn" && !has_token(deps, "gunicorn")) {
|
|
138
|
-
steps.push("pip install --no-cache-dir gunicorn");
|
|
139
|
-
}
|
|
140
|
-
if (server === "uvicorn" && !has_token(deps, "uvicorn")) {
|
|
141
|
-
steps.push("pip install --no-cache-dir uvicorn[standard]");
|
|
142
|
-
}
|
|
143
|
-
if (steps.length === 0) {
|
|
144
|
-
return "true";
|
|
145
|
-
}
|
|
146
|
-
return steps.join(" && ");
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
export { find_app_module, has_token, read_dependencies_text, resolve_start, with_server_injection };
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { DetectError } from './DetectError.js';
|
|
2
|
-
import { docker_buildpack } from './docker/index.js';
|
|
3
|
-
import { node_buildpack } from './node/index.js';
|
|
4
|
-
import { python_buildpack } from './python/index.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Ordered registry, first claim wins. Order is load-bearing:
|
|
8
|
-
* 1. node before python — full-stack repos shipping both a package.json and
|
|
9
|
-
* Python manifests build as node (historical rule).
|
|
10
|
-
* 2. python (all its manifest providers) before docker — a dependency
|
|
11
|
-
* manifest beats a Dockerfile; the Dockerfile is the explicit escape
|
|
12
|
-
* hatch evaluated last among strong triggers.
|
|
13
|
-
* Weak signals (python's entrypoint fallback) run in a second pass so they
|
|
14
|
-
* lose against ANY strong trigger without buildpacks knowing about each other.
|
|
15
|
-
*/
|
|
16
|
-
const BUILDPACKS = [
|
|
17
|
-
node_buildpack,
|
|
18
|
-
python_buildpack,
|
|
19
|
-
docker_buildpack,
|
|
20
|
-
];
|
|
21
|
-
const buildpack_names = () => BUILDPACKS.map((b) => b.name);
|
|
22
|
-
const get_buildpack = (name) => BUILDPACKS.find((b) => b.name === name);
|
|
23
|
-
/**
|
|
24
|
-
* Resolve the buildpack plan for a workdir. `override` (from --buildpack or
|
|
25
|
-
* faable.json) skips detection order and forces one buildpack; it still runs
|
|
26
|
-
* that buildpack's detect so the plan is computed from real project files.
|
|
27
|
-
*/
|
|
28
|
-
const detect_buildpack = async (ctx, override) => {
|
|
29
|
-
if (override) {
|
|
30
|
-
const buildpack = get_buildpack(override);
|
|
31
|
-
if (!buildpack) {
|
|
32
|
-
throw new Error(`Unknown buildpack "${override}". Valid buildpacks: ${buildpack_names().join(", ")}.`);
|
|
33
|
-
}
|
|
34
|
-
const plan = (await buildpack.detect(ctx)) ??
|
|
35
|
-
(await buildpack.detect_fallback?.(ctx)) ??
|
|
36
|
-
null;
|
|
37
|
-
if (!plan) {
|
|
38
|
-
throw new Error(`Buildpack "${override}" was forced but none of its trigger files ` +
|
|
39
|
-
`(${[...buildpack.detect_files, ...(buildpack.fallback_files ?? [])].join(", ")}) ` +
|
|
40
|
-
`exist in ${ctx.workdir}.`);
|
|
41
|
-
}
|
|
42
|
-
return plan;
|
|
43
|
-
}
|
|
44
|
-
// Strong pass: manifests and Dockerfiles.
|
|
45
|
-
for (const buildpack of BUILDPACKS) {
|
|
46
|
-
const plan = await buildpack.detect(ctx);
|
|
47
|
-
if (plan)
|
|
48
|
-
return plan;
|
|
49
|
-
}
|
|
50
|
-
// Fallback pass: weak signals, only when nothing claimed the project.
|
|
51
|
-
for (const buildpack of BUILDPACKS) {
|
|
52
|
-
if (!buildpack.detect_fallback)
|
|
53
|
-
continue;
|
|
54
|
-
const plan = await buildpack.detect_fallback(ctx);
|
|
55
|
-
if (plan)
|
|
56
|
-
return plan;
|
|
57
|
-
}
|
|
58
|
-
throw new DetectError(ctx.workdir, BUILDPACKS);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export { BUILDPACKS, buildpack_names, detect_buildpack, get_buildpack };
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import Handlebars from 'handlebars';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { cmd } from '../../../../lib/cmd.js';
|
|
6
|
-
import { log } from '../../../../log.js';
|
|
7
|
-
|
|
8
|
-
const __filename$1 = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname$1 = path.dirname(__filename$1);
|
|
10
|
-
const templates_dir = path.join(__dirname$1, "templates");
|
|
11
|
-
const dockerfile_source = fs
|
|
12
|
-
.readFileSync(path.join(templates_dir, "Dockerfile"))
|
|
13
|
-
.toString();
|
|
14
|
-
const entrypoint_source = fs
|
|
15
|
-
.readFileSync(path.join(templates_dir, "entrypoint.sh"))
|
|
16
|
-
.toString("utf-8");
|
|
17
|
-
// Backslash-escape content so it survives BOTH the unquoted bash heredoc the
|
|
18
|
-
// Dockerfile is piped through (docker build -f -<<EOF) and, for the entry
|
|
19
|
-
// script, the single-quoted `RUN echo '...'`.
|
|
20
|
-
Handlebars.registerHelper("escape", function (variable) {
|
|
21
|
-
const escaped_lines = variable
|
|
22
|
-
.replace(/(['`\\])/g, "\\$1")
|
|
23
|
-
.replace(/([$])/g, "\\$1");
|
|
24
|
-
return escaped_lines.split("\n").join("\\n");
|
|
25
|
-
});
|
|
26
|
-
const docker_template = Handlebars.compile(dockerfile_source);
|
|
27
|
-
const entrypoint_template = Handlebars.compile(entrypoint_source);
|
|
28
|
-
/** Pure render of the final Dockerfile contents — snapshot-tested. */
|
|
29
|
-
const render_dockerfile = (params) => {
|
|
30
|
-
// NOTE: use slim to build projects
|
|
31
|
-
const from = [params.from, "slim"].filter((e) => e).join("-");
|
|
32
|
-
const entry_script = entrypoint_template({ banner: params.banner });
|
|
33
|
-
return docker_template({
|
|
34
|
-
from,
|
|
35
|
-
env: params.env,
|
|
36
|
-
entry_script,
|
|
37
|
-
start_command: params.start_command,
|
|
38
|
-
install_command: params.install_command,
|
|
39
|
-
install_files: params.install_files,
|
|
40
|
-
});
|
|
41
|
-
};
|
|
42
|
-
const build_image = async (props) => {
|
|
43
|
-
const { app, workdir, dockerfile } = props;
|
|
44
|
-
log.info(`📦 Packaging inside a docker image`);
|
|
45
|
-
const timeout = 10 * 60 * 1000; // 10 minute timeout
|
|
46
|
-
await cmd(`docker build --platform linux/amd64 -t ${app.id} ${workdir} -f -<<EOF\n${dockerfile}\nEOF`, { timeout, enableOutput: true });
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export { build_image, render_dockerfile };
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path__default from 'path';
|
|
3
|
-
|
|
4
|
-
const has_any_of_files = (files, workdir) => {
|
|
5
|
-
for (let name of files) {
|
|
6
|
-
const filename = path__default.join(workdir, name);
|
|
7
|
-
if (fs.existsSync(filename)) {
|
|
8
|
-
return true;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
return false;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export { has_any_of_files };
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Decode a manifest/source buffer to a string, honoring a leading BOM.
|
|
5
|
-
*
|
|
6
|
-
* Editors and shells on Windows (PowerShell `>` redirection, Notepad "Save As")
|
|
7
|
-
* frequently write files as UTF‑16 with a BOM. Reading those as UTF‑8 yields
|
|
8
|
-
* mojibake with interleaved NUL bytes (`f\x00a\x00s\x00t…`), which silently
|
|
9
|
-
* breaks token detection — e.g. `\bfastapi\b` never matches a UTF‑16
|
|
10
|
-
* `requirements.txt`, so the Python buildpack skips framework detection and
|
|
11
|
-
* fails with "Could not detect how to start this Python app". Detect the BOM and
|
|
12
|
-
* decode accordingly, stripping the BOM from the result.
|
|
13
|
-
*/
|
|
14
|
-
const decode_buffer = (buf) => {
|
|
15
|
-
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
|
|
16
|
-
return new TextDecoder("utf-16le").decode(buf.subarray(2));
|
|
17
|
-
}
|
|
18
|
-
if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
|
|
19
|
-
return new TextDecoder("utf-16be").decode(buf.subarray(2));
|
|
20
|
-
}
|
|
21
|
-
if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) {
|
|
22
|
-
return buf.subarray(3).toString("utf8");
|
|
23
|
-
}
|
|
24
|
-
return buf.toString("utf8");
|
|
25
|
-
};
|
|
26
|
-
/** Read a file to a string, decoding UTF‑16/UTF‑8 BOMs (see {@link decode_buffer}). */
|
|
27
|
-
const read_text_file = (file) => decode_buffer(fs.readFileSync(file));
|
|
28
|
-
/** Read and parse a JSON file, tolerating a UTF‑16/UTF‑8 BOM. */
|
|
29
|
-
const read_json_file = (file) => JSON.parse(read_text_file(file));
|
|
30
|
-
|
|
31
|
-
export { decode_buffer, read_json_file, read_text_file };
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
FROM {{from}}
|
|
2
|
-
LABEL com.faable.cloud="FaableCloud"
|
|
3
|
-
LABEL description="Faablecloud automatic deployment"
|
|
4
|
-
|
|
5
|
-
WORKDIR /faable/app
|
|
6
|
-
|
|
7
|
-
# Environment variables for runtime
|
|
8
|
-
ENV PORT=80
|
|
9
|
-
{{#each env}}
|
|
10
|
-
ENV {{@key}}={{this}}
|
|
11
|
-
{{/each}}
|
|
12
|
-
# `escape` keeps `$PORT` literal so the build heredoc (unquoted bash) doesn't
|
|
13
|
-
# eat it; Docker then expands $PORT (=80) when building the ENV value.
|
|
14
|
-
ENV START_COMMAND="{{{escape start_command}}}"
|
|
15
|
-
|
|
16
|
-
{{#if install_files}}
|
|
17
|
-
# Copy dependency manifests first so the install layer stays cached until
|
|
18
|
-
# they change (any source change no longer reinstalls dependencies).
|
|
19
|
-
{{#each install_files}}
|
|
20
|
-
COPY {{this}} ./
|
|
21
|
-
{{/each}}
|
|
22
|
-
RUN {{{escape install_command}}}
|
|
23
|
-
{{/if}}
|
|
24
|
-
|
|
25
|
-
# Copy Usercode
|
|
26
|
-
COPY . .
|
|
27
|
-
|
|
28
|
-
{{#unless install_files}}
|
|
29
|
-
{{#if install_command}}
|
|
30
|
-
# Install needs the full source (e.g. `pip install .`), so it runs after COPY.
|
|
31
|
-
RUN {{{escape install_command}}}
|
|
32
|
-
{{/if}}
|
|
33
|
-
{{/unless}}
|
|
34
|
-
|
|
35
|
-
# Entrypoint stript
|
|
36
|
-
RUN echo '{{{escape entry_script}}}' >> entrypoint.sh
|
|
37
|
-
|
|
38
|
-
CMD ["/bin/sh", "./entrypoint.sh"]
|