Repository: railwayapp/nixpacks
Pull request #1115 (2024-06-09 05:26:14)

feat: dump build command when --out is specified
Follow-up from https://github.com/railwayapp/nixpacks/pull/1066

> Is the use case mainly for debugging what Nixpacks is doing? I wonder if we could instead of a verbose mode where the docker command is output in the logs. Would that solve the problem?

Yes. Primarily for debugging.

Without this, it's hard to actually recreate what nixpacks is doing when building a dockerfile to 
iterate on the Dockerfile config locally.

Outputting the build command would be fine, but it seems better to me to persist the necessary commands to build the
entire docker container exactly like nixpacks does when --dump is used.


iloveitaly: This also fixes a listing error that I believe exists in master 70e0a6ab41f62438191548ec1425cf8046060bef
github-actions[bot]: This pull request is stale because it has been open 10 days with no activity. Remove stale label or comment or this will be closed in 5 days
iloveitaly: Friendly reminder on this pull request! Feel free to let me know if there are any updates needed here.
github-actions[bot]: This pull request is stale because it has been open 10 days with no activity. Remove stale label or comment or this will be closed in 5 days
iloveitaly: Friendly reminder about this pull request! Please let me know if there is anything else that needs to be addressed.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 50163a2..258ab9c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1044,7 +1044,7 @@ dependencies = [
 
 [[package]]
 name = "nixpacks"
-version = "1.24.1"
+version = "1.24.5"
 dependencies = [
  "actix-web",
  "anyhow",
diff --git a/Cargo.toml b/Cargo.toml
index a21a313..af96ccd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "nixpacks"
-version = "1.24.1"
+version = "1.24.5"
 edition = "2021"
 license = "MIT"
 authors = ["Railway <contact@railway.app>"]
diff --git a/docs/pages/docs/providers/node.md b/docs/pages/docs/providers/node.md
index 8ecf5f0..2370f70 100644
--- a/docs/pages/docs/providers/node.md
+++ b/docs/pages/docs/providers/node.md
@@ -21,17 +21,17 @@ The Node provider sets the following environment variables:
 
 The following major versions are available
 
-- `14`
 - `16`
 - `18` (Default)
 - `20`
+- `22`
 
 The version can be overridden by
 
 - Setting the `NIXPACKS_NODE_VERSION` environment variable
 - Specifying the `engines.node` field in `package.json`
 
-Only a major version can be specified. For example, `14.x` or `14`.
+Only a major version can be specified. For example, `18.x` or `20`.
 
 **Node Canvas**
 
@@ -91,11 +91,11 @@ You can specify `cacheDirectories` in `package.json`. Each directory that is pro
 
 Nixpacks has first class support for [Corepack](https://nodejs.org/api/corepack.html), an experimental tool that enables installing specific versions of Node based package managers.
 
-For example, To install a specific version of PNPM, add a `packageManager` key to your `package.json` file
+For example, To install the latest version of PNPM, add a `packageManager` key to your `package.json` file
 
 ```json
 {
-  "packageManager": "pnpm@7.7.0"
+  "packageManager": "pnpm@latest"
 }
 ```
 
diff --git a/docs/pages/docs/providers/php.md b/docs/pages/docs/providers/php.md
index 365f6bd..6f67805 100644
--- a/docs/pages/docs/providers/php.md
+++ b/docs/pages/docs/providers/php.md
@@ -17,7 +17,6 @@ If a `NIXPACKS_PHP_FALLBACK_PATH` variable is passed, that will be used as a fal
 
 The following PHP versions are available
 
-- `8.0`
 - `8.1`
 - `8.2` (Default)
 - `8.3`
diff --git a/docs/pages/docs/providers/python.md b/docs/pages/docs/providers/python.md
index 1e0e046..950a243 100644
--- a/docs/pages/docs/providers/python.md
+++ b/docs/pages/docs/providers/python.md
@@ -15,12 +15,12 @@ Python is detected if any of the following files are found
 
 The following Python versions are available
 
-- `3.11`
-- `3.10`
-- `3.9`
-- `3.8` (Default)
-- `3.7`
 - `2.7`
+- `3.8`
+- `3.9`
+- `3.10`
+- `3.11` (Default)
+- `3.12`
 
 The version can be overridden by
 
diff --git a/docs/sidebar.ts b/docs/sidebar.ts
index 45e79ba..e9c2c24 100644
--- a/docs/sidebar.ts
+++ b/docs/sidebar.ts
@@ -44,6 +44,7 @@ export const sidebarItems: ISidebarSection[] = [
       { href: "/docs/providers/deno", text: "Deno" },
       { href: "/docs/providers/elixir", text: "Elixir" },
       { href: "/docs/providers/fsharp", text: "F#" },
+      { href: "/docs/providers/gleam", text: "Gleam" },
       { href: "/docs/providers/go", text: "Go" },
       { href: "/docs/providers/haskell", text: "Haskell" },
       { href: "/docs/providers/java", text: "Java" },
diff --git a/examples/python-asdf-poetry/.tool-versions b/examples/python-asdf-poetry/.tool-versions
new file mode 100644
index 0000000..8a8ea6f
--- /dev/null
+++ b/examples/python-asdf-poetry/.tool-versions
@@ -0,0 +1,2 @@
+python 3.12.3
+poetry 1.8.2
\ No newline at end of file
diff --git a/examples/python-asdf-poetry/main.py b/examples/python-asdf-poetry/main.py
new file mode 100644
index 0000000..109925d
--- /dev/null
+++ b/examples/python-asdf-poetry/main.py
@@ -0,0 +1,9 @@
+import sys
+import subprocess
+
+print(sys.version)
+
+poetry_version = subprocess.run(
+    ["poetry", "--version"], capture_output=True, text=True
+).stdout.strip()
+print(poetry_version)
diff --git a/examples/python-asdf-poetry/poetry.lock b/examples/python-asdf-poetry/poetry.lock
new file mode 100644
index 0000000..20aad95
--- /dev/null
+++ b/examples/python-asdf-poetry/poetry.lock
@@ -0,0 +1,129 @@
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
+
+[[package]]
+name = "atomicwrites"
+version = "1.4.1"
+description = "Atomic file writes."
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+    {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
+]
+
+[[package]]
+name = "attrs"
+version = "23.2.0"
+description = "Classes Without Boilerplate"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
+    {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
+]
+
+[package.extras]
+cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
+dev = ["attrs[tests]", "pre-commit"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
+tests = ["attrs[tests-no-zope]", "zope-interface"]
+tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
+tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+    {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+    {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "more-itertools"
+version = "10.2.0"
+description = "More routines for operating on iterables, beyond itertools"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"},
+    {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"},
+]
+
+[[package]]
+name = "packaging"
+version = "24.0"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
+    {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
+]
+
+[[package]]
+name = "pluggy"
+version = "0.13.1"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+    {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
+    {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+
+[[package]]
+name = "py"
+version = "1.11.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+    {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
+    {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
+]
+
+[[package]]
+name = "pytest"
+version = "5.4.3"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.5"
+files = [
+    {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
+    {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
+]
+
+[package.dependencies]
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
+attrs = ">=17.4.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+more-itertools = ">=4.0.0"
+packaging = "*"
+pluggy = ">=0.12,<1.0"
+py = ">=1.5.0"
+wcwidth = "*"
+
+[package.extras]
+checkqa-mypy = ["mypy (==v0.761)"]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.13"
+description = "Measures the displayed width of unicode strings in a terminal"
+optional = false
+python-versions = "*"
+files = [
+    {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
+    {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
+]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.10"
+content-hash = "491a37bfc34281cb87606e36384453db66e94ff9c309c7a34be6f9380c5e9782"
diff --git a/examples/python-asdf-poetry/pyproject.toml b/examples/python-asdf-poetry/pyproject.toml
new file mode 100644
index 0000000..d78db5e
--- /dev/null
+++ b/examples/python-asdf-poetry/pyproject.toml
@@ -0,0 +1,15 @@
+[tool.poetry]
+name = "python-asdf-poetry"
+version = "0.1.0"
+description = ""
+authors = ["iloveitaly"]
+
+[tool.poetry.dependencies]
+python = "^3.10"
+
+[tool.poetry.dev-dependencies]
+pytest = "^5.2"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/flake.nix b/flake.nix
index 2761662..d8d0d72 100644
--- a/flake.nix
+++ b/flake.nix
@@ -20,7 +20,7 @@
         let
           package = with nixpkgs; rustPlatform.buildRustPackage {
             pname = "nixpacks";
-            version = "1.24.1";
+            version = "1.24.5";
             src = ./.;
             cargoLock = {
               lockFile = ./Cargo.lock;
diff --git a/src/nixpacks/asdf.rs b/src/nixpacks/asdf.rs
new file mode 100644
index 0000000..d7a2e1d
--- /dev/null
+++ b/src/nixpacks/asdf.rs
@@ -0,0 +1,45 @@
+use regex::Regex;
+use std::collections::HashMap;
+
+pub fn parse_tool_versions_content(file_content: &str) -> HashMap<String, String> {
+    let re = Regex::new(r"\s+").unwrap();
+    file_content
+        .lines()
+        .map(str::trim)
+        .filter(|line| !line.starts_with('#'))
+        .filter_map(|line| {
+            let parts: Vec<&str> = re.splitn(line, 2).collect();
+            if parts.len() == 2 {
+                Some((parts[0].to_string(), parts[1].to_string()))
+            } else {
+                None
+            }
+        })
+        .collect()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_tool_versions_content() {
+        let file_content = "\
+# This is a comment
+python 3.10.4
+ poetry  1.7.1
+# exact ruby version required for homebrew
+ruby\t3.1.4
+direnv  2.32.3 ";
+        let versions = parse_tool_versions_content(file_content);
+
+        let expected = HashMap::from([
+            ("direnv".to_string(), "2.32.3".to_string()),
+            ("python".to_string(), "3.10.4".to_string()),
+            ("ruby".to_string(), "3.1.4".to_string()),
+            ("poetry".to_string(), "1.7.1".to_string()),
+        ]);
+
+        assert_eq!(versions, expected);
+    }
+}
diff --git a/src/nixpacks/builder/docker/docker_image_builder.rs b/src/nixpacks/builder/docker/docker_image_builder.rs
index af15c95..9237b10 100644
--- a/src/nixpacks/builder/docker/docker_image_builder.rs
+++ b/src/nixpacks/builder/docker/docker_image_builder.rs
@@ -37,6 +37,18 @@ fn get_output_dir(app_src: &str, options: &DockerBuilderOptions) -> Result<Outpu
     }
 }
 
+fn command_to_string(command: &Command) -> String {
+    let args = command
+        .get_args()
+        .map(|arg| arg.to_string_lossy())
+        .collect::<Vec<_>>();
+    format!(
+        "{} {}",
+        command.get_program().to_string_lossy(),
+        args.join(" ")
+    )
+}
+
 use async_trait::async_trait;
 
 #[async_trait]
@@ -78,10 +90,17 @@ impl ImageBuilder for DockerImageBuilder {
         plan.write_supporting_files(&self.options, env, &output)
             .context("Writing supporting files")?;
 
+        let mut docker_build_cmd = self.get_docker_build_cmd(plan, name.as_str(), &output)?;
+
+        if self.options.out_dir.is_some() {
+            let command_path = output.get_absolute_path("build.sh");
+            File::create(command_path.clone()).context("Creating command.sh file")?;
+            fs::write(command_path, command_to_string(&docker_build_cmd))
+                .context("Write command")?;
+        }
+
         // Only build if the --out flag was not specified
         if self.options.out_dir.is_none() {
-            let mut docker_build_cmd = self.get_docker_build_cmd(plan, name.as_str(), &output)?;
-
             // Execute docker build
             let build_result = docker_build_cmd.spawn()?.wait().context("Building image")?;
             if !build_result.success() {
diff --git a/src/nixpacks/images.rs b/src/nixpacks/images.rs
index 9edd5ee..a9138d9 100644
--- a/src/nixpacks/images.rs
+++ b/src/nixpacks/images.rs
@@ -1,5 +1,5 @@
-pub const DEBIAN_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:debian-1716249803";
-pub const UBUNTU_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:ubuntu-1716249803";
+pub const DEBIAN_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:debian-1721088222";
+pub const UBUNTU_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:ubuntu-1721088222";
 pub const DEFAULT_BASE_IMAGE: &str = UBUNTU_BASE_IMAGE;
 
 pub const STANDALONE_IMAGE: &str = "ubuntu:jammy";
diff --git a/src/nixpacks/mod.rs b/src/nixpacks/mod.rs
index dd34516..318dc79 100644
--- a/src/nixpacks/mod.rs
+++ b/src/nixpacks/mod.rs
@@ -1,4 +1,5 @@
 pub mod app;
+pub mod asdf;
 pub mod builder;
 pub mod environment;
 mod files;
diff --git a/src/providers/python.rs b/src/providers/python.rs
index 00aca4b..7bb643e 100644
--- a/src/providers/python.rs
+++ b/src/providers/python.rs
@@ -2,6 +2,7 @@ use crate::{
     chain,
     nixpacks::{
         app::App,
+        asdf::parse_tool_versions_content,
         environment::{Environment, EnvironmentVariables},
         plan::{
             phase::{Phase, StartPhase},
@@ -73,12 +74,24 @@ impl Provider for PythonProvider {
         plan.add_variables(PythonProvider::default_python_environment_variables());
 
         if app.includes_file("poetry.lock") {
+            let mut version = POETRY_VERSION.to_string();
+
+            if app.includes_file(".tool-versions") {
+                let file_content = &app.read_file(".tool-versions")?;
+
+                if let Some(poetry_version) =
+                    PythonProvider::parse_tool_versions_poetry_version(file_content)?
+                {
+                    println!("Using poetry version from .tool-versions: {poetry_version}");
+                    version = poetry_version;
+                }
+            }
+
             plan.add_variables(EnvironmentVariables::from([(
                 "NIXPACKS_POETRY_VERSION".to_string(),
-                POETRY_VERSION.to_string(),
+                version,
             )]));
         }
-
         if app.includes_file("pdm.lock") {
             plan.add_variables(EnvironmentVariables::from([(
                 "NIXPACKS_PDM_VERSION".to_string(),
@@ -302,6 +315,32 @@ impl PythonProvider {
             .map(|m| m.get(2).unwrap().as_str().to_string()))
     }
 
+    fn parse_tool_versions_python_version(file_content: &str) -> Result<Option<String>> {
+        let asdf_versions = parse_tool_versions_content(file_content);
+
+        // the python version can only specify a major.minor version right now, and not a patch version
+        // however, in asdf a patch version is specified, so we need to strip it
+        Ok(asdf_versions.get("python").map(|s| {
+            let parts: Vec<&str> = s.split('.').collect();
+
+            if parts.len() == 3 {
+                // this is the expected result, but will be unexpected to users
+                println!("Patch version detected in .tool-versions, but not supported in nixpkgs.");
+            } else if parts.len() == 2 {
+                println!("Expected a version string in the format x.y.z from .tool-versions");
+            } else {
+                println!("Could not find a version string in the format x.y.z or x.y from .tool-versions");
+            }
+
+            format!("{}.{}", parts[0], parts[1])
+        }))
+    }
+
+    fn parse_tool_versions_poetry_version(file_content: &str) -> Result<Option<String>> {
+        let asdf_versions = parse_tool_versions_content(file_content);
+        Ok(asdf_versions.get("poetry").cloned())
+    }
+
     fn default_python_environment_variables() -> EnvironmentVariables {
         let python_variables = vec![
             ("PYTHONFAULTHANDLER", "1"),
@@ -342,6 +381,9 @@ impl PythonProvider {
         } else if app.includes_file("Pipfile") {
             let file_content = &app.read_file("Pipfile")?;
             custom_version = PythonProvider::parse_pipfile_python_version(file_content)?;
+        } else if app.includes_file(".tool-versions") {
+            let file_content = &app.read_file(".tool-versions")?;
+            custom_version = PythonProvider::parse_tool_versions_python_version(file_content)?;
         }
 
         // If it's still none, return default
diff --git a/src/providers/scala.rs b/src/providers/scala.rs
index bf07d18..489dbb9 100644
--- a/src/providers/scala.rs
+++ b/src/providers/scala.rs
@@ -91,8 +91,8 @@ impl ScalaProvider {
 
     fn get_jdk_run_image(&self, jdk_version: u32) -> &str {
         match jdk_version {
-            21 => "eclipse-temurim:21.0.1_12-jre-jammy",
-            20 => "eclipse-temurim:20.0.2_9-jre-jammy",
+            21 => "eclipse-temurin:21.0.1_12-jre-jammy",
+            20 => "eclipse-temurin:20.0.2_9-jre-jammy",
             19 => "eclipse-temurin:19.0.2_7-jre-jammy",
             11 => "eclipse-temurin:11.0.21_9-jre-jammy",
             8 => "eclipse-temurin:8u392-b08-jre-jammy",


Test patch:
diff --git a/tests/docker_run_tests.rs b/tests/docker_run_tests.rs
index a7f915d..7ed1c8a 100644
--- a/tests/docker_run_tests.rs
+++ b/tests/docker_run_tests.rs
@@ -692,6 +692,15 @@ async fn test_python_2() {
     assert!(output.contains("Hello from Python 2"));
 }
 
+#[tokio::test]
+async fn test_python_asdf_poetry() {
+    let name = simple_build("./examples/python-asdf-poetry").await;
+    let output = run_image(&name, None).await;
+
+    assert!(output.contains("3.12.3"), "{}", output);
+    assert!(output.contains("Poetry (version 1.8.2)"), "{}", output);
+}
+
 #[tokio::test]
 async fn test_django() {
     // Create the network
diff --git a/tests/snapshots/generate_plan_tests__python_asdf_poetry.snap b/tests/snapshots/generate_plan_tests__python_asdf_poetry.snap
new file mode 100644
index 0000000..a1af3f3
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__python_asdf_poetry.snap
@@ -0,0 +1,52 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "providers": [],
+  "buildImage": "[build_image]",
+  "variables": {
+    "NIXPACKS_METADATA": "python,poetry",
+    "NIXPACKS_POETRY_VERSION": "1.8.2",
+    "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 poetry==$NIXPACKS_POETRY_VERSION && poetry install --no-dev --no-interaction --no-ansi"
+      ],
+      "cacheDirectories": [
+        "/root/.cache/pip"
+      ],
+      "paths": [
+        "/opt/venv/bin"
+      ]
+    },
+    "setup": {
+      "name": "setup",
+      "nixPkgs": [
+        "python312",
+        "gcc"
+      ],
+      "nixLibs": [
+        "zlib",
+        "stdenv.cc.cc.lib"
+      ],
+      "nixOverlays": [],
+      "nixpkgsArchive": "[archive]"
+    }
+  },
+  "start": {
+    "cmd": "python main.py"
+  }
+}