From 3ab7ccba3dcff2ac71ff35a4d710aef7a5619650 Mon Sep 17 00:00:00 2001 From: Laurentiu Ciobanu Date: Wed, 10 Sep 2025 11:11:45 +0000 Subject: [PATCH 01/11] silence output for internal usage --- src/lib.rs | 18 ++---------------- .../builder/docker/docker_image_builder.rs | 13 +++++-------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c6349d962..e59d04707 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,26 +133,12 @@ pub async fn create_docker_image( let phase_count = plan.phases.clone().map_or(0, |phases| phases.len()); if phase_count > 0 { - println!("{}", plan.get_build_string()?); - let start = plan.start_phase.clone().unwrap_or_default(); if start.cmd.is_none() && !build_options.no_error_without_start { bail!("No start command could be found") } } else { - println!("\nNixpacks was unable to generate a build plan for this app.\nPlease check the documentation for supported languages: https://nixpacks.com"); - println!("\nThe contents of the app directory are:\n"); - - for file in &app.paths { - let path = app.strip_source_path(file.as_path())?; - println!( - " {}{}", - path.display(), - if file.is_dir() { "/" } else { "" } - ); - } - - std::process::exit(1); + bail!("Failed to generate a build plan. Check the documentation for auto-build supported targets: https://docs.lttle.cloud/build/auto-build#supported-targets") } if build_options.out_dir.is_none() { @@ -170,7 +156,7 @@ fn ensure_docker_exists() -> Result<()> { let mut docker_build_cmd = Command::new("docker"); if docker_build_cmd.output().is_err() { - bail!("Please install Docker to build the app https://docs.docker.com/engine/install/"); + bail!("Docker is required to build the app. Check the prerequisites here: https://docs.lttle.cloud/build/auto-build#requirements"); } Ok(()) diff --git a/src/nixpacks/builder/docker/docker_image_builder.rs b/src/nixpacks/builder/docker/docker_image_builder.rs index 610f1e073..1c67574fb 100644 --- a/src/nixpacks/builder/docker/docker_image_builder.rs +++ b/src/nixpacks/builder/docker/docker_image_builder.rs @@ -13,14 +13,13 @@ use crate::nixpacks::{ use anyhow::{bail, Context, Ok, Result}; use std::{ fs::{self, remove_dir_all, File}, - process::Command, + process::{Command, Stdio}, }; use tempdir::TempDir; use uuid::Uuid; /// Builds Docker images from options, logging to stdout if the build is successful. pub struct DockerImageBuilder { - logger: Logger, options: DockerBuilderOptions, } @@ -106,10 +105,6 @@ impl ImageBuilder for DockerImageBuilder { bail!("Docker build failed") } - self.logger.log_section("Successfully Built!"); - println!("\nRun:"); - println!(" docker run -it {name}"); - if self.options.incremental_cache_image.is_some() { incremental_cache.create_image( &incremental_cache_dirs, @@ -130,8 +125,8 @@ impl ImageBuilder for DockerImageBuilder { } impl DockerImageBuilder { - pub fn new(logger: Logger, options: DockerBuilderOptions) -> DockerImageBuilder { - DockerImageBuilder { logger, options } + pub fn new(_logger: Logger, options: DockerBuilderOptions) -> DockerImageBuilder { + DockerImageBuilder { options } } /// Generates the Docker command and arguments for building the project. @@ -167,6 +162,8 @@ impl DockerImageBuilder { } if self.options.quiet { + docker_build_cmd.stdout(Stdio::null()); + docker_build_cmd.stderr(Stdio::null()); docker_build_cmd.arg("--quiet"); } From 8556099603559f2c296d992fdd61db2fde6fb900 Mon Sep 17 00:00:00 2001 From: Laurentiu Ciobanu Date: Wed, 10 Sep 2025 11:17:27 +0000 Subject: [PATCH 02/11] change default node version to 22 --- src/providers/node/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs index 7238ea866..047f491fa 100644 --- a/src/providers/node/mod.rs +++ b/src/providers/node/mod.rs @@ -32,7 +32,7 @@ const BUN_NIXPKGS_ARCHIVE: &str = "5a0711127cd8b916c3d3128f473388c8c79df0da"; // We need to use a specific commit hash for Node versions <16 since it is EOL in the latest Nix packages const NODE_LT_16_ARCHIVE: &str = "bf744fe90419885eefced41b3e5ae442d732712d"; -const DEFAULT_NODE_VERSION: u32 = 18; +const DEFAULT_NODE_VERSION: u32 = 22; const AVAILABLE_NODE_VERSIONS: &[u32] = &[14, 16, 18, 20, 22, 23]; const YARN_CACHE_DIR: &str = "/usr/local/share/.cache/yarn/v6"; From 3831df965d2103eb92deab3b4e90136f41f904bb Mon Sep 17 00:00:00 2001 From: Laurentiu Ciobanu Date: Wed, 10 Sep 2025 21:16:04 +0000 Subject: [PATCH 03/11] give more structured output from plun build str --- src/nixpacks/plan/pretty_print.rs | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/nixpacks/plan/pretty_print.rs b/src/nixpacks/plan/pretty_print.rs index de5427753..c42d13456 100644 --- a/src/nixpacks/plan/pretty_print.rs +++ b/src/nixpacks/plan/pretty_print.rs @@ -9,7 +9,45 @@ const FIRST_COLUMN_MIN_WIDTH: usize = 10; const MIN_BOX_WIDTH: usize = 20; const MAX_BOX_WIDTH: usize = 80; +#[derive(Debug, Clone)] +pub struct PhaseInfoDesc { + pub phases: Vec<(String, Vec)>, + pub start: String, +} + impl BuildPlan { + pub fn get_phase_info_desc(&self) -> Result { + let phase_contents = self + .get_sorted_phases()? + .iter() + .filter(|phase| phase.uses_nix() || phase.apt_pkgs.is_some() || phase.cmds.is_some()) + .map(|phase| { + ( + phase.get_name(), + self.get_phase_content(phase) + .unwrap() + .split('\n') + .map(|s| s.to_string()) + .collect::>(), + ) + }) + .collect::>(); + + let start_contents = self + .start_phase + .clone() + .unwrap_or_default() + .cmd + .unwrap_or_default(); + + let info = PhaseInfoDesc { + phases: phase_contents, + start: start_contents, + }; + + Ok(info) + } + /// The pretty-printed build plan, emitted by `nixpacks build`. pub fn get_build_string(&self) -> Result { let title_str = format!(" Nixpacks v{NIX_PACKS_VERSION} "); From 26919cada94dba845ae1afd3ccb63e068ba8d194 Mon Sep 17 00:00:00 2001 From: Laurentiu Ciobanu Date: Sun, 14 Sep 2025 19:27:06 +0000 Subject: [PATCH 04/11] ensure yarn, pnpm, bun install dev packages during build --- src/providers/node/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs index 047f491fa..b2f1493d4 100644 --- a/src/providers/node/mod.rs +++ b/src/providers/node/mod.rs @@ -456,14 +456,14 @@ impl NodeProvider { let mut install_cmd = "npm i".to_string(); let package_manager = NodeProvider::get_package_manager(app); if package_manager == "pnpm" { - install_cmd = "pnpm i --frozen-lockfile".to_string(); + install_cmd = "pnpm i --frozen-lockfile --prod=false".to_string(); } else if package_manager == "yarn" { // TODO: When using Corepack and modern Yarn, we may not have a .yarnrc.yml - need to // read the Yarn version from stdout after enabling Corepack. if app.includes_file(".yarnrc.yml") { - install_cmd = "yarn install --check-cache".to_string(); + install_cmd = "yarn install --check-cache --production=false".to_string(); } else { - install_cmd = "yarn install --frozen-lockfile".to_string(); + install_cmd = "yarn install --frozen-lockfile --production=false".to_string(); } } else if app.includes_file("package-lock.json") { install_cmd = "npm ci".to_string(); From 4ec7164f5b938c6fe26ff3978ae08bb106bad91b Mon Sep 17 00:00:00 2001 From: Laurentiu Ciobanu Date: Fri, 26 Sep 2025 15:32:17 +0000 Subject: [PATCH 05/11] remove verbose log --- src/nixpacks/builder/docker/docker_image_builder.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/nixpacks/builder/docker/docker_image_builder.rs b/src/nixpacks/builder/docker/docker_image_builder.rs index 1c67574fb..6319a0454 100644 --- a/src/nixpacks/builder/docker/docker_image_builder.rs +++ b/src/nixpacks/builder/docker/docker_image_builder.rs @@ -115,9 +115,6 @@ impl ImageBuilder for DockerImageBuilder { if output.is_temp { remove_dir_all(output.root)?; } - } else { - println!("\nSaved output to:"); - println!(" {}", output.root.to_str().unwrap()); } Ok(()) From 23e7086cb5b6f9328aefa63f02aa636dcae3aed1 Mon Sep 17 00:00:00 2001 From: Laurentiu Ciobanu Date: Sat, 27 Sep 2025 14:45:42 +0000 Subject: [PATCH 06/11] always copy src files to out dir --- src/nixpacks/builder/docker/docker_image_builder.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/nixpacks/builder/docker/docker_image_builder.rs b/src/nixpacks/builder/docker/docker_image_builder.rs index 6319a0454..ebd81a496 100644 --- a/src/nixpacks/builder/docker/docker_image_builder.rs +++ b/src/nixpacks/builder/docker/docker_image_builder.rs @@ -226,11 +226,7 @@ impl DockerImageBuilder { /// Copies project files to temporary output dir, if that option was used. fn write_app(&self, app_src: &str, output: &OutputDir) -> Result<()> { - if output.is_temp { - files::recursive_copy_dir(app_src, &output.root) - } else { - Ok(()) - } + files::recursive_copy_dir(app_src, &output.root) } /// Writes the generated Dockerfile to the output dir. From 21c49c8b2c46eda88d7fde39377d3b68edf3e49e Mon Sep 17 00:00:00 2001 From: Laurentiu Ciobanu Date: Sat, 27 Sep 2025 15:29:19 +0000 Subject: [PATCH 07/11] force dev deps isntall for npm --- src/providers/node/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs index b2f1493d4..a54f5021b 100644 --- a/src/providers/node/mod.rs +++ b/src/providers/node/mod.rs @@ -466,7 +466,7 @@ impl NodeProvider { install_cmd = "yarn install --frozen-lockfile --production=false".to_string(); } } else if app.includes_file("package-lock.json") { - install_cmd = "npm ci".to_string(); + install_cmd = "npm ci --include=dev".to_string(); } else if app.includes_file("bun.lockb") || app.includes_file("bun.lock") { install_cmd = "bun i --no-save".to_string(); } From 5aa1442eea54d8353fdae11d9a04fc85fa876bfb Mon Sep 17 00:00:00 2001 From: Laurentiu Ciobanu Date: Sat, 27 Sep 2025 15:41:24 +0000 Subject: [PATCH 08/11] force dev deps isntall for npm --- src/providers/node/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs index a54f5021b..3bedc59e5 100644 --- a/src/providers/node/mod.rs +++ b/src/providers/node/mod.rs @@ -453,7 +453,7 @@ impl NodeProvider { return None; } - let mut install_cmd = "npm i".to_string(); + let mut install_cmd = "npm i --include=dev".to_string(); let package_manager = NodeProvider::get_package_manager(app); if package_manager == "pnpm" { install_cmd = "pnpm i --frozen-lockfile --prod=false".to_string(); @@ -466,7 +466,7 @@ impl NodeProvider { install_cmd = "yarn install --frozen-lockfile --production=false".to_string(); } } else if app.includes_file("package-lock.json") { - install_cmd = "npm ci --include=dev".to_string(); + install_cmd = "npm i --include=dev --no-package-lock".to_string(); } else if app.includes_file("bun.lockb") || app.includes_file("bun.lock") { install_cmd = "bun i --no-save".to_string(); } From 6f7257184eb96d6eda35ec1a552dbec007b2a09e Mon Sep 17 00:00:00 2001 From: Laurentiu Ciobanu Date: Mon, 29 Sep 2025 16:16:46 +0000 Subject: [PATCH 09/11] handle bun install for bun projects --- src/providers/node/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs index 3bedc59e5..d7b17934c 100644 --- a/src/providers/node/mod.rs +++ b/src/providers/node/mod.rs @@ -465,10 +465,10 @@ impl NodeProvider { } else { install_cmd = "yarn install --frozen-lockfile --production=false".to_string(); } + } else if package_manager == "bun" { + install_cmd = "bun i --no-save".to_string(); } else if app.includes_file("package-lock.json") { install_cmd = "npm i --include=dev --no-package-lock".to_string(); - } else if app.includes_file("bun.lockb") || app.includes_file("bun.lock") { - install_cmd = "bun i --no-save".to_string(); } Some(install_cmd) From f2ac2b18d6be79ad815c714c894ea776245a031e Mon Sep 17 00:00:00 2001 From: Laurentiu Ciobanu Date: Tue, 30 Sep 2025 07:23:00 +0000 Subject: [PATCH 10/11] speed up cross-arch npm installs --- src/providers/node/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs index d7b17934c..ebb57187c 100644 --- a/src/providers/node/mod.rs +++ b/src/providers/node/mod.rs @@ -468,7 +468,8 @@ impl NodeProvider { } else if package_manager == "bun" { install_cmd = "bun i --no-save".to_string(); } else if app.includes_file("package-lock.json") { - install_cmd = "npm i --include=dev --no-package-lock".to_string(); + // a lock-file might include some deps arch dependent. so we need to rebuild them. + install_cmd = "npm ci --ignore-scripts --no-audit --no-fund && npm rebuild --no-audit --no-fund".to_string(); } Some(install_cmd) From 30e5c096d856f3d36a5df82e8c0ad4509cb4fc7e Mon Sep 17 00:00:00 2001 From: Laurentiu Ciobanu Date: Tue, 14 Oct 2025 16:58:27 +0000 Subject: [PATCH 11/11] improve python start cmd --- src/providers/python.rs | 50 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/providers/python.rs b/src/providers/python.rs index 7a5dad8fb..419f00843 100644 --- a/src/providers/python.rs +++ b/src/providers/python.rs @@ -120,15 +120,21 @@ impl Provider for PythonProvider { fn metadata(&self, app: &App, env: &Environment) -> Result { let is_django = PythonProvider::is_django(app, env)?; + let is_flask = PythonProvider::is_flask(app)?; + let is_fasthtml = PythonProvider::is_fasthtml(app)?; let is_using_postgres = PythonProvider::is_using_postgres(app, env)?; let is_poetry = app.includes_file("poetry.lock"); let is_pdm = app.includes_file("pdm.lock"); + let is_uv = app.includes_file("uv.lock"); Ok(ProviderMetadata::from(vec![ (is_django, "django"), + (is_flask, "flask"), + (is_fasthtml, "fasthtml"), (is_using_postgres, "postgres"), (is_poetry, "poetry"), (is_pdm, "pdm"), + (is_uv, "uv"), ])) } @@ -383,20 +389,40 @@ impl PythonProvider { } fn start(&self, app: &App, env: &Environment) -> Result> { + // Priority 1: Django with gunicorn and migrations if PythonProvider::is_django(app, env)? { let app_name = PythonProvider::get_django_app_name(app, env)?; - return Ok(Some(StartPhase::new(format!( - "python manage.py migrate && gunicorn {app_name}" + "python manage.py migrate && gunicorn {app_name}.wsgi:application --bind 0.0.0.0:${{PORT:-8000}}" )))); } - // the python package is extracted from pyproject.toml, but this can often not be the desired entrypoint - // for this reason we prefer main.py to the module heuristic used in the pyproject.toml logic + // Priority 2: FastHTML with uvicorn (only checks main.py specifically) + if PythonProvider::is_fasthtml(app)? + && app.includes_file("main.py") + && PythonProvider::uses_dep(app, "uvicorn")? + { + return Ok(Some(StartPhase::new( + "uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}".to_string() + ))); + } + + // Priority 3: Flask with gunicorn (only checks main.py specifically) + if PythonProvider::is_flask(app)? + && app.includes_file("main.py") + && PythonProvider::uses_dep(app, "gunicorn")? + { + return Ok(Some(StartPhase::new( + "gunicorn --bind 0.0.0.0:${PORT:-8000} main:app".to_string() + ))); + } + + // Priority 4: main.py specifically (preserves original behavior) if app.includes_file("main.py") { return Ok(Some(StartPhase::new("python main.py".to_string()))); } + // Priority 5: PyProject entry points (original priority preserved) if app.includes_file("pyproject.toml") { if let OkResult(meta) = PythonProvider::parse_pyproject(app) { if let Some(entry_point) = meta.entry_point { @@ -408,6 +434,14 @@ impl PythonProvider { } } + // Priority 6: Other common entry files (new fallback) + let other_files = ["app.py", "bot.py", "hello.py", "server.py"]; + for file in other_files { + if app.includes_file(file) { + return Ok(Some(StartPhase::new(format!("python {file}")))); + } + } + Ok(None) } @@ -418,6 +452,14 @@ impl PythonProvider { Ok(has_manage && imports_django) } + fn is_fasthtml(app: &App) -> Result { + PythonProvider::uses_dep(app, "python-fasthtml") + } + + fn is_flask(app: &App) -> Result { + PythonProvider::uses_dep(app, "flask") + } + fn is_using_postgres(app: &App, _env: &Environment) -> Result { // Check for the engine database type in settings.py let re = Regex::new(r"django.db.backends.postgresql").unwrap();