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..ebd81a496 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, @@ -120,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(()) @@ -130,8 +122,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 +159,8 @@ impl DockerImageBuilder { } if self.options.quiet { + docker_build_cmd.stdout(Stdio::null()); + docker_build_cmd.stderr(Stdio::null()); docker_build_cmd.arg("--quiet"); } @@ -232,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. 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} "); diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs index 7238ea866..ebb57187c 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"; @@ -453,22 +453,23 @@ 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".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(); - } else if app.includes_file("bun.lockb") || app.includes_file("bun.lock") { + } else if package_manager == "bun" { install_cmd = "bun i --no-save".to_string(); + } else if app.includes_file("package-lock.json") { + // 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) 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();