diff --git a/Cargo.lock b/Cargo.lock index 3fb9b9265..3aa821da7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1075,7 +1075,7 @@ dependencies = [ [[package]] name = "nixpacks" -version = "1.30.0" +version = "1.31.0" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index c017b7e4b..2d1a0e66d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nixpacks" -version = "1.30.0" +version = "1.31.0" edition = "2021" license = "MIT" authors = ["Railway "] diff --git a/docs/pages/docs/deploying/stacktape.md b/docs/pages/docs/deploying/stacktape.md new file mode 100644 index 000000000..63a75b536 --- /dev/null +++ b/docs/pages/docs/deploying/stacktape.md @@ -0,0 +1,9 @@ +--- +title: Stacktape +--- + +# {% $markdoc.frontmatter.title %} + +[Stacktape](https://stacktape.com?ref=nixpacks) allows you to use Nixpacks as a builder for web applications, workers, and other container workloads. You can configure the Nixpacks build with nixpacks config (`nixpacks.json` or `nixpacks.toml`) or directly in [Stacktape config](https://docs.stacktape.com/configuration/packaging/#nixpacks) or console. + +![](/images/stacktape.png) diff --git a/docs/public/images/stacktape.png b/docs/public/images/stacktape.png new file mode 100644 index 000000000..8d8ca6505 Binary files /dev/null and b/docs/public/images/stacktape.png differ diff --git a/examples/lunatic-basic/.cargo/config.toml b/examples/lunatic-basic/.cargo/config.toml deleted file mode 100644 index 7d2003277..000000000 --- a/examples/lunatic-basic/.cargo/config.toml +++ /dev/null @@ -1,5 +0,0 @@ -[build] -target = "wasm32-wasi" - -[target.wasm32-wasi] -runner = "lunatic" diff --git a/examples/lunatic-basic/Cargo.toml b/examples/lunatic-basic/Cargo.toml deleted file mode 100644 index ae285b76f..000000000 --- a/examples/lunatic-basic/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "lunatic-basic" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -lunatic = "0.13" diff --git a/examples/lunatic-basic/src/main.rs b/examples/lunatic-basic/src/main.rs deleted file mode 100644 index 30e69c7d3..000000000 --- a/examples/lunatic-basic/src/main.rs +++ /dev/null @@ -1,21 +0,0 @@ -use lunatic::net; - -fn main() { - let sender = net::UdpSocket::bind("127.0.0.1:0").unwrap(); - let receiver = net::UdpSocket::bind("127.0.0.1:0").unwrap(); - let receiver_addr = receiver.local_addr().unwrap(); - - sender.connect(receiver_addr).expect("couldn't connect"); - sender - .send("P1NG".as_bytes()) - .expect("couldn't send message"); - - let mut buf = [0; 4]; - let len_in = receiver.recv(&mut buf).unwrap(); - - assert_eq!(len_in, 4); - assert_eq!(buf, "P1NG".as_bytes()); - - // wasi stdout - println!("PING-PONG"); -} diff --git a/examples/python-psycopg2/database.py b/examples/python-psycopg2/database.py new file mode 100644 index 000000000..682a926c0 --- /dev/null +++ b/examples/python-psycopg2/database.py @@ -0,0 +1,30 @@ +import os +import psycopg2 +from psycopg2 import Error +import time + +def get_db_connection(): + max_retries = 3 + retry_delay = 2 # seconds + + for attempt in range(max_retries): + try: + connection = psycopg2.connect( + host=os.getenv('PGHOST'), + database=os.getenv('PGDATABASE'), + user=os.getenv('PGUSER'), + password=os.getenv('PGPASSWORD'), + port=os.getenv('PGPORT') + ) + return connection + except Error as e: + if "starting up" in str(e).lower(): + if attempt < max_retries - 1: # Don't sleep on last attempt + time.sleep(retry_delay) + continue + print(f"Error connecting to PostgreSQL: {e}") + return None + +def close_connection(connection): + if connection: + connection.close() diff --git a/examples/python-psycopg2/main.py b/examples/python-psycopg2/main.py new file mode 100644 index 000000000..0ba5707b6 --- /dev/null +++ b/examples/python-psycopg2/main.py @@ -0,0 +1,83 @@ +from database import get_db_connection, close_connection + +def create_table(): + connection = get_db_connection() + if connection: + try: + cursor = connection.cursor() + + # Create a sample table + create_table_query = ''' + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL + ) + ''' + cursor.execute(create_table_query) + + # Commit the changes + connection.commit() + print("Table created successfully") + + except Exception as e: + print(f"Error: {e}") + finally: + cursor.close() + close_connection(connection) + +def insert_user(name, email): + connection = get_db_connection() + if connection: + try: + cursor = connection.cursor() + + # Insert a new user + insert_query = ''' + INSERT INTO users (name, email) + VALUES (%s, %s) + RETURNING id + ''' + cursor.execute(insert_query, (name, email)) + user_id = cursor.fetchone()[0] + + # Commit the changes + connection.commit() + print(f"User inserted successfully with ID: {user_id}") + + except Exception as e: + print(f"Error: {e}") + finally: + cursor.close() + close_connection(connection) + +def get_all_users(): + connection = get_db_connection() + if connection: + try: + cursor = connection.cursor() + + # Select all users + select_query = "SELECT * FROM users" + cursor.execute(select_query) + users = cursor.fetchall() + + for user in users: + print(f"ID: {user[0]}, Name: {user[1]}, Email: {user[2]}") + + except Exception as e: + print(f"Error: {e}") + finally: + cursor.close() + close_connection(connection) + +if __name__ == "__main__": + # Create the table + create_table() + + # Insert some sample users + insert_user("John Doe", "john@example.com") + insert_user("Jane Smith", "jane@example.com") + + # Retrieve and display all users + get_all_users() diff --git a/examples/python-psycopg2/requirements.txt b/examples/python-psycopg2/requirements.txt new file mode 100644 index 000000000..1b2d42782 --- /dev/null +++ b/examples/python-psycopg2/requirements.txt @@ -0,0 +1 @@ +psycopg2-binary>=2.9.10 diff --git a/flake.nix b/flake.nix index f1e0cd542..1ca90a43d 100644 --- a/flake.nix +++ b/flake.nix @@ -20,7 +20,7 @@ let package = with nixpkgs; rustPlatform.buildRustPackage { pname = "nixpacks"; - version = "1.30.0"; + version = "1.31.0"; src = ./.; cargoLock = { lockFile = ./Cargo.lock; diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs index 9e25ee548..21deab9aa 100644 --- a/src/providers/node/mod.rs +++ b/src/providers/node/mod.rs @@ -672,14 +672,18 @@ fn parse_node_version_into_pkg(node_version: &str) -> String { eprintln!("Warning: node version {node_version} is not valid, using default node version {default_node_pkg_name}"); Range::parse(DEFAULT_NODE_VERSION.to_string()).unwrap() }); - let mut available_node_versions = AVAILABLE_NODE_VERSIONS.to_vec(); + let mut available_lts_node_versions = AVAILABLE_NODE_VERSIONS + .iter() + .filter(|v| *v % 2 == 0) + .collect::>(); + // use newest node version first - available_node_versions.sort_by(|a, b| b.cmp(a)); - for version_number in available_node_versions { + available_lts_node_versions.sort_by(|a, b| b.cmp(a)); + for version_number in available_lts_node_versions { let version_range_string = format!("{version_number}.x.x"); let version_range: Range = version_range_string.parse().unwrap(); if version_range.allows_any(&range) { - return version_number_to_pkg(version_number); + return version_number_to_pkg(*version_number); } } default_node_pkg_name @@ -759,6 +763,24 @@ mod test { Ok(()) } + #[test] + fn test_latest_lts_version() -> Result<()> { + assert_eq!( + NodeProvider::get_nix_node_pkg( + &PackageJson { + name: Some(String::default()), + engines: Some(engines_node(">=18")), + ..Default::default() + }, + &App::new("examples/node")?, + &Environment::default() + )?, + Pkg::new(version_number_to_pkg(22).as_str()) + ); + + Ok(()) + } + #[test] fn test_simple_engine() -> Result<()> { assert_eq!( diff --git a/src/providers/python.rs b/src/providers/python.rs index db7e4bfe6..585e3f9fc 100644 --- a/src/providers/python.rs +++ b/src/providers/python.rs @@ -160,18 +160,15 @@ enum EntryPoint { impl PythonProvider { fn setup(&self, app: &App, env: &Environment) -> Result> { + let mut setup = Phase::setup(None); + let mut pkgs: Vec = vec![]; let (python_base_package, nix_archive) = PythonProvider::get_nix_python_package(app, env)?; pkgs.append(&mut vec![python_base_package]); if PythonProvider::is_using_postgres(app, env)? { - // Postgres requires postgresql and gcc on top of the original python packages - - // .dev variant is required in order for pg_config to be available, which is needed by psycopg2 - // the .dev variant requirement is caused by this change in nix pkgs: - // https://github.com/NixOS/nixpkgs/blob/43eac3c9e618c4114a3441b52949609ea2104670/pkgs/servers/sql/postgresql/pg_config.sh - pkgs.append(&mut vec![Pkg::new("postgresql.dev")]); + pkgs.append(&mut vec![Pkg::new("postgresql_16.dev")]); } if PythonProvider::is_django(app, env)? && PythonProvider::is_using_mysql(app, env)? { @@ -183,7 +180,7 @@ impl PythonProvider { pkgs.append(&mut vec![Pkg::new("pipenv")]); } - let mut setup = Phase::setup(Some(pkgs)); + setup.add_nix_pkgs(&pkgs); setup.set_nix_archive(nix_archive); if PythonProvider::uses_dep(app, "cairo")? { diff --git a/src/providers/rust.rs b/src/providers/rust.rs index 68c6b744c..c91be6946 100644 --- a/src/providers/rust.rs +++ b/src/providers/rust.rs @@ -22,6 +22,8 @@ const CARGO_GIT_CACHE_DIR: &str = "/root/.cargo/git"; const CARGO_REGISTRY_CACHE_DIR: &str = "/root/.cargo/registry"; const CARGO_TARGET_CACHE_DIR: &str = "target"; +const NIX_ARCHIVE: &str = "ef56e777fedaa4da8c66a150081523c5de1e0171"; + pub struct RustProvider {} impl Provider for RustProvider { @@ -76,6 +78,8 @@ impl RustProvider { setup.add_nix_pkgs(&[Pkg::new("musl"), Pkg::new("musl.dev")]); } + setup.set_nix_archive(NIX_ARCHIVE.to_string()); + Ok(setup) } diff --git a/tests/docker_run_tests.rs b/tests/docker_run_tests.rs index 50ef814e2..4c1cc63a6 100644 --- a/tests/docker_run_tests.rs +++ b/tests/docker_run_tests.rs @@ -940,6 +940,48 @@ async fn test_python_asdf_poetry() { assert!(output.contains("Poetry (version 1.8.2)"), "{}", output); } +#[tokio::test] +async fn test_python_psycopg2() -> Result<()> { + // Create the network + let n = create_network(); + let network_name = n.name.clone(); + + // Create the postgres instance + let c = run_postgres(); + let container_name = c.name.clone(); + + // Attach the postgres instance to the network + attach_container_to_network(n.name, container_name.clone()); + + let name = match simple_build("./examples/python-psycopg2").await { + Ok(name) => name, + Err(err) => { + // Cleanup containers and networks, and then error + stop_and_remove_container(container_name); + remove_network(network_name); + return Err(err); + } + }; + + let output = run_image( + &name, + Some(Config { + environment_variables: c.config.unwrap().environment_variables, + network: Some(network_name.clone()), + }), + ) + .await; + + println!("OUTPUT = {}", output); + + // Cleanup containers and networks + stop_and_remove_container(container_name); + remove_network(network_name); + + assert!(output.contains("User inserted successfully with ID")); + Ok(()) +} + #[tokio::test] async fn test_django() -> Result<()> { // Create the network @@ -1018,13 +1060,6 @@ async fn test_django_mysql() -> Result<()> { Ok(()) } -#[tokio::test] -async fn test_lunatic_basic() { - let name = simple_build("./examples/lunatic-basic").await.unwrap(); - let output = run_image(&name, None).await; - assert!(output.contains("PING-PONG")); -} - #[tokio::test] async fn test_python_poetry() { let name = simple_build("./examples/python-poetry").await.unwrap(); diff --git a/tests/snapshots/generate_plan_tests__lunatic_basic.snap b/tests/snapshots/generate_plan_tests__lunatic_basic.snap index 139e3deae..25e16b1c1 100644 --- a/tests/snapshots/generate_plan_tests__lunatic_basic.snap +++ b/tests/snapshots/generate_plan_tests__lunatic_basic.snap @@ -30,7 +30,7 @@ expression: plan "nixPkgs": [ "binutils", "gcc", - "(rust-bin.stable.latest.default.override { targets = [\"wasm32-wasi\"]; })", + "(rust-bin.stable.\"1.83.0\".default.override { targets = [\"wasm32-wasi\"]; })", "lunatic" ], "nixOverlays": [ diff --git a/tests/snapshots/generate_plan_tests__node_pnpm_corepack.snap b/tests/snapshots/generate_plan_tests__node_pnpm_corepack.snap index c2629e25e..2e4c1a0b5 100644 --- a/tests/snapshots/generate_plan_tests__node_pnpm_corepack.snap +++ b/tests/snapshots/generate_plan_tests__node_pnpm_corepack.snap @@ -1,7 +1,6 @@ --- source: tests/generate_plan_tests.rs expression: plan -snapshot_kind: text --- { "providers": [], @@ -44,7 +43,7 @@ snapshot_kind: text "setup": { "name": "setup", "nixPkgs": [ - "nodejs_23", + "nodejs_22", "pnpm-9_x" ], "nixOverlays": [ diff --git a/tests/snapshots/generate_plan_tests__node_turborepo.snap b/tests/snapshots/generate_plan_tests__node_turborepo.snap index 4550bd278..3c634e673 100644 --- a/tests/snapshots/generate_plan_tests__node_turborepo.snap +++ b/tests/snapshots/generate_plan_tests__node_turborepo.snap @@ -1,7 +1,6 @@ --- source: tests/generate_plan_tests.rs expression: plan -snapshot_kind: text --- { "providers": [], @@ -45,7 +44,7 @@ snapshot_kind: text "setup": { "name": "setup", "nixPkgs": [ - "nodejs_23", + "nodejs_22", "npm-8_x" ], "nixOverlays": [ diff --git a/tests/snapshots/generate_plan_tests__python_django.snap b/tests/snapshots/generate_plan_tests__python_django.snap index 4821f83dd..8ab07114f 100644 --- a/tests/snapshots/generate_plan_tests__python_django.snap +++ b/tests/snapshots/generate_plan_tests__python_django.snap @@ -1,7 +1,6 @@ --- source: tests/generate_plan_tests.rs expression: plan -snapshot_kind: text --- { "providers": [], @@ -36,7 +35,7 @@ snapshot_kind: text "name": "setup", "nixPkgs": [ "python3", - "postgresql.dev", + "postgresql_16.dev", "gcc" ], "nixLibs": [ diff --git a/tests/snapshots/generate_plan_tests__python_postgres.snap b/tests/snapshots/generate_plan_tests__python_postgres.snap index 622bd3e09..1b3651629 100644 --- a/tests/snapshots/generate_plan_tests__python_postgres.snap +++ b/tests/snapshots/generate_plan_tests__python_postgres.snap @@ -1,7 +1,6 @@ --- source: tests/generate_plan_tests.rs expression: plan -snapshot_kind: text --- { "providers": [], @@ -36,7 +35,7 @@ snapshot_kind: text "name": "setup", "nixPkgs": [ "python3", - "postgresql.dev", + "postgresql_16.dev", "gcc" ], "nixLibs": [ diff --git a/tests/snapshots/generate_plan_tests__python_psycopg2.snap b/tests/snapshots/generate_plan_tests__python_psycopg2.snap new file mode 100644 index 000000000..1b3651629 --- /dev/null +++ b/tests/snapshots/generate_plan_tests__python_psycopg2.snap @@ -0,0 +1,52 @@ +--- +source: tests/generate_plan_tests.rs +expression: plan +--- +{ + "providers": [], + "buildImage": "[build_image]", + "variables": { + "NIXPACKS_METADATA": "python,postgres", + "PIP_DEFAULT_TIMEOUT": "100", + "PIP_DISABLE_PIP_VERSION_CHECK": "1", + "PIP_NO_CACHE_DIR": "1", + "PYTHONDONTWRITEBYTECODE": "1", + "PYTHONFAULTHANDLER": "1", + "PYTHONHASHSEED": "random", + "PYTHONUNBUFFERED": "1" + }, + "phases": { + "install": { + "name": "install", + "dependsOn": [ + "setup" + ], + "cmds": [ + "python -m venv --copies /opt/venv && . /opt/venv/bin/activate && pip install -r requirements.txt" + ], + "cacheDirectories": [ + "/root/.cache/pip" + ], + "paths": [ + "/opt/venv/bin" + ] + }, + "setup": { + "name": "setup", + "nixPkgs": [ + "python3", + "postgresql_16.dev", + "gcc" + ], + "nixLibs": [ + "zlib", + "stdenv.cc.cc.lib" + ], + "nixOverlays": [], + "nixpkgsArchive": "[archive]" + } + }, + "start": { + "cmd": "python main.py" + } +}