Repository: railwayapp/nixpacks
Pull request #200 (2022-06-10 04:41:33)

Auto-bump base image using github actions
This PR adds a new step in [`Publish Docker Images`](https://github.com/railwayapp/nixpacks/actions/workflows/publish_debian.yml) workflow which will update debian image to latest tag whenever a new release is published.


Patch:
diff --git a/.github/workflows/publish_debian.yml b/.github/workflows/publish_debian.yml
index 5c45eb6..7415085 100644
--- a/.github/workflows/publish_debian.yml
+++ b/.github/workflows/publish_debian.yml
@@ -16,7 +16,8 @@ jobs:
   debian:
     runs-on: ubuntu-latest
     permissions:
-      contents: read
+      contents: write
+      pull-requests: write
       packages: write
       id-token: write
 
@@ -37,15 +38,31 @@ jobs:
           registry: ghcr.io
           username: ${{ github.actor }}
           password: ${{ secrets.GITHUB_TOKEN }}
-      -
-        name: Get current date
+      
+      - name: Get current date
         id: date
         run: echo "::set-output name=date::$(date +%s)"
-      - 
-        name: Build and push
+      
+      - name: Build and push
         uses: docker/build-push-action@v3
         with:
           context: base/debian
           platforms: linux/arm64, linux/amd64, linux/386
-          push: ${{ github.event_name != 'pull_request' }}
+          push: ${{ github.event_name == 'push' }}
           tags: ghcr.io/railwayapp/nixpacks:debian, ghcr.io/railwayapp/nixpacks:latest, ghcr.io/railwayapp/nixpacks:debian-${{ steps.date.outputs.date }}
+      
+      - name: Bump base image
+        if: ${{ github.event_name == 'push' }}
+        run: |
+          sed -i 's/nixpacks:debian-.*/nixpacks:debian-${{ steps.date.outputs.date }}";/g' src/nixpacks/images.rs
+      
+      - name: Create Pull Request
+        if: ${{ github.event_name == 'push' }}
+        uses: peter-evans/create-pull-request@v4
+        with:
+          base: main
+          token: ${{ secrets.GITHUB_TOKEN }}
+          commit-message: Bump base image
+          branch: debian-${{ steps.date.outputs.date }}
+          delete-branch: true
+          title: Bump base image to `debian-${{ steps.date.outputs.date }}`
diff --git a/Cargo.lock b/Cargo.lock
index d9f750c..088596c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -260,7 +260,7 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
 
 [[package]]
 name = "nixpacks"
-version = "0.0.21"
+version = "0.0.22"
 dependencies = [
  "anyhow",
  "clap",
diff --git a/Cargo.toml b/Cargo.toml
index ab6565e..4944c01 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "nixpacks"
-version = "0.0.21"
+version = "0.0.22"
 edition = "2021"
 license = "MIT"
 authors = ["Railway <contact@railway.app>"]
diff --git a/examples/fsharp-api/Program.fs b/examples/fsharp-api/Program.fs
new file mode 100644
index 0000000..17dded8
--- /dev/null
+++ b/examples/fsharp-api/Program.fs
@@ -0,0 +1,27 @@
+open Microsoft.AspNetCore.Builder
+open System
+
+
+type WeatherForecast = {
+    Date: DateTime
+    TemperatureC: int
+    Summary: string option
+}
+with member x.TemperatureF = 32 + int (float x.TemperatureC / 0.5556)
+
+let Summaries =  ["Freezing"; "Bracing"; "Chilly"; "Cool"; "Mild"; "Warm"; "Balmy"; "Hot"; "Sweltering"; "Scorching"]
+
+let getWeatherForecast() = 
+    [1..5] 
+    |> List.map (fun index -> {
+            Date =  DateTime.Now.AddDays(index)
+            TemperatureC = Random.Shared.Next(-20, 55)
+            Summary = Some Summaries.[Random.Shared.Next(Summaries.Length)]
+        })    
+
+
+let builder = WebApplication.CreateBuilder()
+let app = builder.Build()
+
+app.MapGet("/", Func<WeatherForecast list>(getWeatherForecast)) |> ignore
+app.Run()
\ No newline at end of file
diff --git a/examples/fsharp-api/fsharp-api.fsproj b/examples/fsharp-api/fsharp-api.fsproj
new file mode 100644
index 0000000..f953d8c
--- /dev/null
+++ b/examples/fsharp-api/fsharp-api.fsproj
@@ -0,0 +1,8 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+    <PropertyGroup>
+        <TargetFramework>net6.0</TargetFramework>
+    </PropertyGroup>
+    <ItemGroup>
+        <Compile Include="Program.fs" />
+    </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/examples/fsharp-cli/Program.fs b/examples/fsharp-cli/Program.fs
new file mode 100644
index 0000000..c978681
--- /dev/null
+++ b/examples/fsharp-cli/Program.fs
@@ -0,0 +1,3 @@
+open System
+
+Console.WriteLine "Hello world from F#"
diff --git a/examples/fsharp-cli/fsharp-cli.fsproj b/examples/fsharp-cli/fsharp-cli.fsproj
new file mode 100644
index 0000000..f953d8c
--- /dev/null
+++ b/examples/fsharp-cli/fsharp-cli.fsproj
@@ -0,0 +1,8 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+    <PropertyGroup>
+        <TargetFramework>net6.0</TargetFramework>
+    </PropertyGroup>
+    <ItemGroup>
+        <Compile Include="Program.fs" />
+    </ItemGroup>
+</Project>
\ No newline at end of file
diff --git a/examples/node-no-lockfile-canvas/index.js b/examples/node-no-lockfile-canvas/index.js
new file mode 100644
index 0000000..e8e31fe
--- /dev/null
+++ b/examples/node-no-lockfile-canvas/index.js
@@ -0,0 +1,2 @@
+require("canvas");
+console.log("Hello from Node canvas");
diff --git a/examples/node-no-lockfile/package.json b/examples/node-no-lockfile-canvas/package.json
similarity index 70%
rename from examples/node-no-lockfile/package.json
rename to examples/node-no-lockfile-canvas/package.json
index 2f12d8f..0953c6f 100644
--- a/examples/node-no-lockfile/package.json
+++ b/examples/node-no-lockfile-canvas/package.json
@@ -4,5 +4,8 @@
   "main": "index.js",
   "scripts": {
     "start": "node index.js"
+  },
+  "dependencies": {
+    "canvas": "^2.9.1"
   }
 }
diff --git a/examples/node-no-lockfile/index.js b/examples/node-no-lockfile/index.js
deleted file mode 100644
index bc8aaff..0000000
--- a/examples/node-no-lockfile/index.js
+++ /dev/null
@@ -1,1 +0,0 @@
-console.log("Hello from Node");
diff --git a/src/lib.rs b/src/lib.rs
index 0faa950..04a62b4 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -15,8 +15,9 @@ use crate::nixpacks::{
 use anyhow::Result;
 use providers::{
     crystal::CrystalProvider, csharp::CSharpProvider, dart::DartProvider, deno::DenoProvider,
-    go::GolangProvider, haskell::HaskellStackProvider, java::JavaProvider, node::NodeProvider,
-    python::PythonProvider, rust::RustProvider, staticfile::StaticfileProvider, Provider,
+    fsharp::FSharpProvider, go::GolangProvider, haskell::HaskellStackProvider, java::JavaProvider,
+    node::NodeProvider, python::PythonProvider, rust::RustProvider, staticfile::StaticfileProvider,
+    Provider,
 };
 
 mod chain;
@@ -32,6 +33,7 @@ pub fn get_providers() -> Vec<&'static dyn Provider> {
         &PythonProvider {},
         &HaskellStackProvider {},
         &CSharpProvider {},
+        &FSharpProvider {},
         &CrystalProvider {},
         &StaticfileProvider {},
         &DartProvider {},
diff --git a/src/main.rs b/src/main.rs
index c5b6c5e..53b6af1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -94,6 +94,14 @@ fn main() -> Result<()> {
                 .multiple_values(true)
                 .global(true),
         )
+        .arg(
+            Arg::new("libs")
+                .long("libs")
+                .help("Provide additional nix libraries to install in the environment")
+                .takes_value(true)
+                .multiple_values(true)
+                .global(true),
+        )
         .arg(
             Arg::new("pin")
                 .long("pin")
@@ -118,6 +126,15 @@ fn main() -> Result<()> {
         Some(values) => values.map(Pkg::new).collect::<Vec<_>>(),
         None => Vec::new(),
     };
+    let libs = matches
+        .value_of("libs")
+        .map(|lib_string| {
+            lib_string
+                .split(' ')
+                .map(|s| s.to_string())
+                .collect::<Vec<String>>()
+        })
+        .unwrap_or_default();
     let pin_pkgs = matches.is_present("pin");
 
     let envs: Vec<_> = match matches.values_of("env") {
@@ -132,6 +149,7 @@ fn main() -> Result<()> {
         custom_start_cmd: start_cmd,
         custom_build_cmd: build_cmd,
         custom_pkgs: pkgs,
+        custom_libs: libs,
         pin_pkgs,
         plan_path,
     };
diff --git a/src/nixpacks/images.rs b/src/nixpacks/images.rs
index 4a354e9..b1a6816 100644
--- a/src/nixpacks/images.rs
+++ b/src/nixpacks/images.rs
@@ -1,3 +1,3 @@
-pub static DEFAULT_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:debian-1654798583";
+pub static DEFAULT_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:debian-1654800960";
 
 pub static DEBIAN_SLIM_IMAGE: &str = "debian:bullseye-slim";
diff --git a/src/nixpacks/phase.rs b/src/nixpacks/phase.rs
index fde2bb0..2745562 100644
--- a/src/nixpacks/phase.rs
+++ b/src/nixpacks/phase.rs
@@ -47,12 +47,11 @@ impl SetupPhase {
         self.archive = Some(archive);
     }
 
-    pub fn add_library(&mut self, lib: String) {
-        if let Some(mut libraries) = self.libraries.clone() {
-            libraries.push(lib);
-            self.libraries = Some(libraries);
+    pub fn add_libraries(&mut self, lib: Vec<String>) {
+        if let Some(libraries) = self.libraries.clone() {
+            self.libraries = Some([libraries, lib].concat());
         } else {
-            self.libraries = Some(vec![lib]);
+            self.libraries = Some(lib);
         }
     }
 }
diff --git a/src/nixpacks/plan/generator.rs b/src/nixpacks/plan/generator.rs
index 76dff55..7cf94c0 100644
--- a/src/nixpacks/plan/generator.rs
+++ b/src/nixpacks/plan/generator.rs
@@ -22,6 +22,7 @@ pub struct GeneratePlanOptions {
     pub custom_build_cmd: Option<String>,
     pub custom_start_cmd: Option<String>,
     pub custom_pkgs: Vec<Pkg>,
+    pub custom_libs: Vec<String>,
     pub pin_pkgs: bool,
     pub plan_path: Option<String>,
 }
@@ -116,6 +117,19 @@ impl<'a> NixpacksBuildPlanGenerator<'a> {
         let mut pkgs = [self.options.custom_pkgs.clone(), env_var_pkgs].concat();
         setup_phase.add_pkgs(&mut pkgs);
 
+        let env_var_libs = environment
+            .get_config_variable("LIBS")
+            .map(|lib_string| {
+                lib_string
+                    .split(' ')
+                    .map(|s| s.to_string())
+                    .collect::<Vec<String>>()
+            })
+            .unwrap_or_default();
+
+        // Add custom user libraries
+        let libs = [self.options.custom_libs.clone(), env_var_libs].concat();
+        setup_phase.add_libraries(libs);
         if self.options.pin_pkgs {
             setup_phase.set_archive(NIXPKGS_ARCHIVE.to_string())
         }
diff --git a/src/providers/fsharp.rs b/src/providers/fsharp.rs
new file mode 100644
index 0000000..111a1c8
--- /dev/null
+++ b/src/providers/fsharp.rs
@@ -0,0 +1,72 @@
+use super::Provider;
+use crate::nixpacks::{
+    app::App,
+    environment::{Environment, EnvironmentVariables},
+    nix::pkg::Pkg,
+    phase::{BuildPhase, InstallPhase, SetupPhase, StartPhase},
+};
+use anyhow::{Context, Result};
+
+pub struct FSharpProvider {}
+
+pub const ARTIFACT_DIR: &'static &str = &"out";
+
+impl Provider for FSharpProvider {
+    fn name(&self) -> &str {
+        "fsharp"
+    }
+
+    fn detect(&self, app: &App, _env: &Environment) -> Result<bool> {
+        Ok(!app.find_files("*.fsproj")?.is_empty())
+    }
+
+    fn setup(&self, _app: &App, _env: &Environment) -> Result<Option<SetupPhase>> {
+        Ok(Some(SetupPhase::new(vec![Pkg::new("dotnet-sdk")])))
+    }
+
+    fn install(&self, _app: &App, _env: &Environment) -> Result<Option<InstallPhase>> {
+        Ok(Some(InstallPhase::new("dotnet restore".to_string())))
+    }
+
+    fn build(&self, _app: &App, _env: &Environment) -> Result<Option<BuildPhase>> {
+        Ok(Some(BuildPhase::new(format!(
+            "dotnet publish --no-restore -c Release -o {}",
+            ARTIFACT_DIR
+        ))))
+    }
+
+    fn start(&self, app: &App, _env: &Environment) -> Result<Option<StartPhase>> {
+        let fsproj = &app.find_files("*.fsproj")?[0].with_extension("");
+        let project_name = fsproj
+            .file_name()
+            .context("Invalid file_name")?
+            .to_str()
+            .context("Invalid project_name")?;
+        Ok(Some(StartPhase::new(format!(
+            "./{}/{}",
+            ARTIFACT_DIR, project_name
+        ))))
+    }
+
+    fn environment_variables(
+        &self,
+        _app: &App,
+        _env: &Environment,
+    ) -> Result<Option<EnvironmentVariables>> {
+        let env_vars = EnvironmentVariables::from([
+            (
+                "ASPNETCORE_ENVIRONMENT".to_string(),
+                "Production".to_string(),
+            ),
+            (
+                "ASPNETCORE_URLS".to_string(),
+                "http://0.0.0.0:3000".to_string(),
+            ),
+            (
+                "DOTNET_ROOT".to_string(),
+                "/nix/var/nix/profiles/default/".to_string(),
+            ),
+        ]);
+        Ok(Some(env_vars))
+    }
+}
diff --git a/src/providers/mod.rs b/src/providers/mod.rs
index bb47933..af5f167 100644
--- a/src/providers/mod.rs
+++ b/src/providers/mod.rs
@@ -9,6 +9,7 @@ pub mod crystal;
 pub mod csharp;
 pub mod dart;
 pub mod deno;
+pub mod fsharp;
 pub mod go;
 pub mod haskell;
 pub mod java;
diff --git a/src/providers/node.rs b/src/providers/node.rs
index 9d3ef8a..28edf29 100644
--- a/src/providers/node.rs
+++ b/src/providers/node.rs
@@ -36,7 +36,11 @@ impl Provider for NodeProvider {
 
     fn setup(&self, app: &App, env: &Environment) -> Result<Option<SetupPhase>> {
         let packages = NodeProvider::get_nix_packages(app, env)?;
-        Ok(Some(SetupPhase::new(packages)))
+        let mut setup_phase = SetupPhase::new(packages);
+        if NodeProvider::uses_canvas(app) {
+            setup_phase.add_libraries(vec!["libuuid".to_string(), "libGL".to_string()]);
+        }
+        Ok(Some(setup_phase))
     }
 
     fn install(&self, app: &App, _env: &Environment) -> Result<Option<InstallPhase>> {
@@ -177,24 +181,34 @@ impl NodeProvider {
     pub fn get_nix_packages(app: &App, env: &Environment) -> Result<Vec<Pkg>> {
         let package_json: PackageJson = app.read_json("package.json")?;
         let node_pkg = NodeProvider::get_nix_node_pkg(&package_json, env)?;
-
+        let mut pkgs = vec![node_pkg.clone()];
         if NodeProvider::get_package_manager(app) == "pnpm" {
             let mut pnpm_pkg = Pkg::new("nodePackages.pnpm");
             // Only override the node package if not the default one
             if node_pkg.name != *DEFAULT_NODE_PKG_NAME {
                 pnpm_pkg = pnpm_pkg.set_override("nodejs", node_pkg.name.as_str());
             }
-            Ok(vec![node_pkg, pnpm_pkg])
+            pkgs.push(pnpm_pkg);
         } else if NodeProvider::get_package_manager(app) == "yarn" {
             let mut yarn_pkg = Pkg::new("yarn");
             // Only override the node package if not the default one
             if node_pkg.name != *DEFAULT_NODE_PKG_NAME {
                 yarn_pkg = yarn_pkg.set_override("nodejs", node_pkg.name.as_str());
             }
-            Ok(vec![node_pkg, yarn_pkg])
-        } else {
-            Ok(vec![node_pkg])
+            pkgs.push(yarn_pkg);
         }
+        Ok(pkgs)
+    }
+
+    pub fn uses_canvas(app: &App) -> bool {
+        let package_json = app.read_file("package.json").unwrap_or_default();
+        let lock_json = app.read_file("package-lock.json").unwrap_or_default();
+        let yarn_lock = app.read_file("yarn.lock").unwrap_or_default();
+        let pnpm_yaml = app.read_file("pnpm-lock.yaml").unwrap_or_default();
+        package_json.contains("\"canvas\"")
+            || lock_json.contains("/canvas/")
+            || yarn_lock.contains("/canvas/")
+            || pnpm_yaml.contains("/canvas/")
     }
 }
 
diff --git a/src/providers/python.rs b/src/providers/python.rs
index fb07302..fa18e04 100644
--- a/src/providers/python.rs
+++ b/src/providers/python.rs
@@ -156,7 +156,11 @@ enum EntryPoint {
 
 impl PythonProvider {
     fn is_django(app: &App, _env: &Environment) -> Result<bool> {
-        Ok(app.includes_file("manage.py") && app.read_file("requirements.txt")?.contains("Django"))
+        Ok(app.includes_file("manage.py")
+            && app
+                .read_file("requirements.txt")?
+                .to_lowercase()
+                .contains("django"))
     }
 
     fn is_using_postgres(app: &App, _env: &Environment) -> Result<bool> {


Test patch:
diff --git a/tests/docker_run_tests.rs b/tests/docker_run_tests.rs
index 31f91ad..ab774c2 100644
--- a/tests/docker_run_tests.rs
+++ b/tests/docker_run_tests.rs
@@ -238,9 +238,9 @@ fn test_node_custom_version() {
 
 #[test]
 fn test_node_no_lockfile() {
-    let name = simple_build("./examples/node-no-lockfile");
+    let name = simple_build("./examples/node-no-lockfile-canvas");
     let output = run_image(name, None);
-    assert!(output.contains("Hello from Node"));
+    assert!(output.contains("Hello from Node canvas"));
 }
 
 #[test]
@@ -285,6 +285,13 @@ fn test_csharp() {
     assert!(output.contains("Hello world from C#"));
 }
 
+#[test]
+fn test_fsharp() {
+    let name = simple_build("./examples/fsharp-cli");
+    let output = run_image(name, None);
+    assert!(output.contains("Hello world from F#"));
+}
+
 #[test]
 fn test_python() {
     let name = simple_build("./examples/python");
diff --git a/tests/generate_plan_tests.rs b/tests/generate_plan_tests.rs
index 394eb1b..6085611 100644
--- a/tests/generate_plan_tests.rs
+++ b/tests/generate_plan_tests.rs
@@ -23,7 +23,7 @@ fn test_node() -> Result<()> {
 
 #[test]
 fn test_node_no_lockfile() -> Result<()> {
-    let plan = simple_gen_plan("./examples/node-no-lockfile");
+    let plan = simple_gen_plan("./examples/node-no-lockfile-canvas");
     assert_eq!(plan.install.unwrap().cmd, Some("npm i".to_string()));
     assert_eq!(plan.build.unwrap().cmd, None);
     assert_eq!(plan.start.unwrap().cmd, Some("npm run start".to_string()));
@@ -218,6 +218,25 @@ fn test_csharp_api() -> Result<()> {
     Ok(())
 }
 
+#[test]
+fn test_fsharp_api() -> Result<()> {
+    let plan = simple_gen_plan("./examples/fsharp-api");
+    assert_eq!(
+        plan.install.unwrap().cmd,
+        Some("dotnet restore".to_string())
+    );
+    assert_eq!(
+        plan.build.unwrap().cmd,
+        Some("dotnet publish --no-restore -c Release -o out".to_string())
+    );
+    assert_eq!(
+        plan.start.unwrap().cmd,
+        Some("./out/fsharp-api".to_string())
+    );
+
+    Ok(())
+}
+
 #[test]
 fn test_csharp_cli() -> Result<()> {
     let plan = simple_gen_plan("./examples/csharp-cli");