Repository: railwayapp/nixpacks (rust)

Issue:
Support `rust-version`
You can use the `rust-version` field on `package` in `Cargo.toml`: [(docs)](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field)

Pull request:
Rust package versions and Nix overlays
The main goal of this PR was to allow customization of the rust version installed. A few things came out of that effort

- New `--out|-o` flag that can be passed to the `build` command. This will output to a specific directory instead of building with Docker. It is mainly useful for debugging the generated Nix and Docker commands, but can also be used to deploy to Railway.
- Change the Nix expression that we are generating. It now uses `stdenv.buildEnv` and loads that with `nix-env -if`
- Rename the provider `pkgs` method to `nix_config`, which returns a `NixConfig` struct instead of just a list of packages to install. Overlays can be added to this config which will be used
- Parse the `rust-version` field in the Rust `Cargo.toml` file. We are also setup to use the `rust-toolchain` file, but it leads into caching so I will follow up with that.

For example, the `environment.nix` file that is generated for a custom rust version is

![image](https://user-images.githubusercontent.com/3044853/162523647-f74fbd2a-7768-41a2-bd11-0756ade3a7ca.png)

I'm sure this can and will be improved in the future.

The rust overlay we use is https://github.com/oxalica/rust-overlay

Closes #14 

Patch:
diff --git a/.gitignore b/.gitignore
index 7dadccc..e0e1d08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,4 @@ Cargo.lock
 tmp/
 testing
 nix-testing
+test
diff --git a/examples/rust-custom-version/.gitignore b/examples/rust-custom-version/.gitignore
new file mode 100644
index 0000000..8c88100
--- /dev/null
+++ b/examples/rust-custom-version/.gitignore
@@ -0,0 +1,13 @@
+# Created by https://www.toptal.com/developers/gitignore/api/rust
+# Edit at https://www.toptal.com/developers/gitignore?templates=rust
+
+### Rust ###
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+
+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
+Cargo.lock
+
+# End of https://www.toptal.com/developers/gitignore/api/rust
diff --git a/examples/rust-custom-version/Cargo.toml b/examples/rust-custom-version/Cargo.toml
new file mode 100644
index 0000000..7690d09
--- /dev/null
+++ b/examples/rust-custom-version/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "rust-custom-version"
+version = "0.1.0"
+edition = "2021"
+rust-version = "1.56.0"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
diff --git a/examples/rust-custom-version/src/main.rs b/examples/rust-custom-version/src/main.rs
new file mode 100644
index 0000000..bcc90a9
--- /dev/null
+++ b/examples/rust-custom-version/src/main.rs
@@ -0,0 +1,6 @@
+use std::process::Command;
+
+fn main() {
+    let mut cargo_version_command = Command::new("cargo").arg("version").spawn().unwrap();
+    cargo_version_command.wait().unwrap();
+}
diff --git a/src/lib.rs b/src/lib.rs
index f3eb530..12712d0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,8 +1,8 @@
-use std::{env, fs};
+use std::env;
 
 use crate::{
     nixpacks::{
-        app::App, environment::Environment, logger::Logger, pkg::Pkg, plan::BuildPlan, AppBuilder,
+        app::App, environment::Environment, logger::Logger, nix::Pkg, plan::BuildPlan, AppBuilder,
         AppBuilderOptions,
     },
     providers::{
@@ -10,7 +10,7 @@ use crate::{
         yarn::YarnProvider,
     },
 };
-use anyhow::{bail, Context, Result};
+use anyhow::{bail, Result};
 use providers::Provider;
 
 pub mod nixpacks;
@@ -42,6 +42,8 @@ pub fn gen_plan(
         custom_build_cmd,
         custom_start_cmd,
         pin_pkgs,
+        out_dir: None,
+        plan_path: None,
     };
 
     let app = App::new(path)?;
@@ -61,7 +63,8 @@ pub fn build(
     custom_start_cmd: Option<String>,
     pin_pkgs: bool,
     envs: Vec<&str>,
-    plan_path: Option<&str>,
+    plan_path: Option<String>,
+    out_dir: Option<String>,
 ) -> Result<()> {
     let logger = Logger::new();
     let providers = get_providers();
@@ -71,23 +74,15 @@ pub fn build(
         custom_build_cmd,
         custom_start_cmd,
         pin_pkgs,
+        out_dir,
+        plan_path,
     };
 
     let app = App::new(path)?;
     let environment = create_environment(envs)?;
     let mut app_builder = AppBuilder::new(name, &app, &environment, &logger, &options)?;
 
-    match plan_path {
-        Some(plan_path) => {
-            let plan_json = fs::read_to_string(plan_path).context("Reading build plan")?;
-            let plan: BuildPlan =
-                serde_json::from_str(&plan_json).context("Deserializing build plan")?;
-            app_builder.build_from_plan(&plan)?;
-        }
-        None => {
-            app_builder.build(providers)?;
-        }
-    }
+    app_builder.build(providers)?;
 
     Ok(())
 }
diff --git a/src/main.rs b/src/main.rs
index b6080e2..39da187 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -27,6 +27,13 @@ fn main() -> Result<()> {
                         .long("plan")
                         .help("Existing build plan file to use")
                         .takes_value(true),
+                )
+                .arg(
+                    Arg::new("out")
+                        .long("out")
+                        .short('o')
+                        .help("Save output directory instead of building it with Docker")
+                        .takes_value(true),
                 ),
         )
         .arg(
@@ -95,10 +102,11 @@ fn main() -> Result<()> {
         Some(("build", matches)) => {
             let path = matches.value_of("PATH").expect("required");
             let name = matches.value_of("name").map(|n| n.to_string());
-            let plan_path = matches.value_of("plan");
+            let plan_path = matches.value_of("plan").map(|n| n.to_string());
+            let output_dir = matches.value_of("out").map(|n| n.to_string());
 
             build(
-                path, name, pkgs, build_cmd, start_cmd, pin_pkgs, envs, plan_path,
+                path, name, pkgs, build_cmd, start_cmd, pin_pkgs, envs, plan_path, output_dir,
             )?;
         }
         _ => eprintln!("Invalid command"),
diff --git a/src/nixpacks/mod.rs b/src/nixpacks/mod.rs
index 6acf8b1..66c843c 100644
--- a/src/nixpacks/mod.rs
+++ b/src/nixpacks/mod.rs
@@ -3,7 +3,7 @@ use indoc::formatdoc;
 use std::{
     fs::{self, File},
     io::Write,
-    path::{Path, PathBuf},
+    path::PathBuf,
     process::Command,
 };
 use tempdir::TempDir;
@@ -11,7 +11,7 @@ use uuid::Uuid;
 pub mod app;
 pub mod environment;
 pub mod logger;
-pub mod pkg;
+pub mod nix;
 pub mod plan;
 
 use crate::providers::Provider;
@@ -20,7 +20,7 @@ use self::{
     app::App,
     environment::{Environment, EnvironmentVariables},
     logger::Logger,
-    pkg::Pkg,
+    nix::{NixConfig, Pkg},
     plan::BuildPlan,
 };
 
@@ -35,6 +35,8 @@ pub struct AppBuilderOptions {
     pub custom_start_cmd: Option<String>,
     pub custom_pkgs: Vec<Pkg>,
     pub pin_pkgs: bool,
+    pub out_dir: Option<String>,
+    pub plan_path: Option<String>,
 }
 
 impl AppBuilderOptions {
@@ -44,6 +46,8 @@ impl AppBuilderOptions {
             custom_start_cmd: None,
             custom_pkgs: Vec::new(),
             pin_pkgs: false,
+            out_dir: None,
+            plan_path: None,
         }
     }
 }
@@ -79,7 +83,7 @@ impl<'a> AppBuilder<'a> {
         // Load options from the best matching provider
         self.detect(providers).context("Detecting provider")?;
 
-        let pkgs = self.get_pkgs().context("Getting packages")?;
+        let nix_config = self.get_nix_config().context("Getting packages")?;
         let install_cmd = self
             .get_install_cmd()
             .context("Generating install command")?;
@@ -89,12 +93,7 @@ impl<'a> AppBuilder<'a> {
 
         let plan = BuildPlan {
             version: NIX_PACKS_VERSION.to_string(),
-            nixpkgs_archive: if self.options.pin_pkgs {
-                Some(NIXPKGS_ARCHIVE.to_string())
-            } else {
-                None
-            },
-            pkgs,
+            nix_config,
             install_cmd,
             start_cmd,
             build_cmd,
@@ -106,22 +105,36 @@ impl<'a> AppBuilder<'a> {
 
     pub fn build(&mut self, providers: Vec<&'a dyn Provider>) -> Result<()> {
         self.logger.log_section("Building");
-        let plan = self.plan(providers).context("Creating build plan")?;
-        self.logger.log_step("Generated new build plan");
 
-        self.do_build(&plan)
-    }
+        let plan = match &self.options.plan_path {
+            Some(plan_path) => {
+                self.logger.log_step("Building from existing plan");
+                let plan_json = fs::read_to_string(plan_path).context("Reading build plan")?;
+                let plan: BuildPlan =
+                    serde_json::from_str(&plan_json).context("Deserializing build plan")?;
+                plan
+            }
+            None => {
+                self.logger.log_step("Generated new build plan");
 
-    pub fn build_from_plan(&mut self, plan: &BuildPlan) -> Result<()> {
-        self.logger.log_section("Building");
-        self.logger.log_step("Building from existing plan");
+                self.plan(providers).context("Creating build plan")?
+            }
+        };
 
-        self.do_build(plan)
+        self.do_build(&plan)
     }
 
     pub fn do_build(&mut self, plan: &BuildPlan) -> Result<()> {
         let id = Uuid::new_v4();
-        let tmp_dir = TempDir::new("nixpacks").context("Creating a temp directory")?;
+
+        let dir: String = match &self.options.out_dir {
+            Some(dir) => dir.clone(),
+            None => {
+                let tmp = TempDir::new("nixpacks").context("Creating a temp directory")?;
+                let path = tmp.path().to_str().unwrap();
+                path.to_string()
+            }
+        };
 
         self.logger.log_step("Copying source to tmp dir");
 
@@ -129,7 +142,7 @@ impl<'a> AppBuilder<'a> {
         let mut copy_cmd = Command::new("cp")
             .arg("-a")
             .arg(format!("{}/.", source))
-            .arg(tmp_dir.path())
+            .arg(dir.clone())
             .spawn()?;
         let copy_result = copy_cmd.wait().context("Copying app source to tmp dir")?;
         if !copy_result.success() {
@@ -137,45 +150,53 @@ impl<'a> AppBuilder<'a> {
         }
 
         self.logger.log_step("Writing build plan");
-        AppBuilder::write_build_plan(plan, tmp_dir.path()).context("Writing build plan")?;
+        AppBuilder::write_build_plan(plan, dir.as_str()).context("Writing build plan")?;
 
         self.logger.log_step("Building image");
 
         let name = self.name.clone().unwrap_or_else(|| id.to_string());
 
-        let mut docker_build_cmd = Command::new("docker")
-            .arg("build")
-            .arg(tmp_dir.path())
-            .arg("-t")
-            .arg(name.clone())
-            .spawn()?;
+        if self.options.out_dir.is_none() {
+            let mut docker_build_cmd = Command::new("docker")
+                .arg("build")
+                .arg(dir)
+                .arg("-t")
+                .arg(name.clone())
+                .spawn()?;
 
-        let build_result = docker_build_cmd.wait().context("Building image")?;
+            let build_result = docker_build_cmd.wait().context("Building image")?;
 
-        if !build_result.success() {
-            bail!("Docker build failed")
-        }
+            if !build_result.success() {
+                bail!("Docker build failed")
+            }
 
-        self.logger.log_section("Successfully Built!");
+            self.logger.log_section("Successfully Built!");
 
-        println!("\nRun:");
-        println!("  docker run -it {}", name);
+            println!("\nRun:");
+            println!("  docker run -it {}", name);
+        } else {
+            println!("\nSaved output to:");
+            println!("  {}", dir);
+        };
 
         Ok(())
     }
 
-    fn get_pkgs(&self) -> Result<Vec<Pkg>> {
-        let pkgs: Vec<Pkg> = match self.provider {
+    fn get_nix_config(&self) -> Result<NixConfig> {
+        let config: NixConfig = match self.provider {
             Some(provider) => {
-                let mut provider_pkgs = provider.pkgs(self.app, self.environment)?;
+                let config = provider.nix_config(self.app, self.environment)?;
                 let mut pkgs = self.options.custom_pkgs.clone();
-                pkgs.append(&mut provider_pkgs);
-                pkgs
+                config.add_pkgs(&mut pkgs)
             }
-            None => self.options.custom_pkgs.clone(),
+            None => NixConfig::new(self.options.custom_pkgs.clone()),
         };
 
-        Ok(pkgs)
+        if self.options.pin_pkgs {
+            Ok(config.set_archive(NIXPKGS_ARCHIVE.to_string()))
+        } else {
+            Ok(config)
+        }
     }
 
     fn get_variables(&self) -> Result<EnvironmentVariables> {
@@ -264,7 +285,7 @@ impl<'a> AppBuilder<'a> {
         }
     }
 
-    pub fn write_build_plan(plan: &BuildPlan, dest: &Path) -> Result<()> {
+    pub fn write_build_plan(plan: &BuildPlan, dest: &str) -> Result<()> {
         let nix_expression = AppBuilder::gen_nix(plan).context("Generating Nix expression")?;
         let dockerfile = AppBuilder::gen_dockerfile(plan).context("Generating Dockerfile")?;
 
@@ -283,26 +304,50 @@ impl<'a> AppBuilder<'a> {
 
     pub fn gen_nix(plan: &BuildPlan) -> Result<String> {
         let nixpkgs = plan
+            .nix_config
             .pkgs
             .iter()
             .map(|p| p.to_nix_string())
             .collect::<Vec<String>>()
             .join(" ");
 
-        let nix_archive = plan.nixpkgs_archive.clone();
+        let nix_archive = plan.nix_config.nixpkgs_archive.clone();
         let pkg_import = match nix_archive {
             Some(archive) => format!(
-                "with import (fetchTarball \"https://github.com/NixOS/nixpkgs/archive/{}.tar.gz\")",
+                "import (fetchTarball \"https://github.com/NixOS/nixpkgs/archive/{}.tar.gz\")",
                 archive
             ),
-            None => "with import <nixpkgs>".to_string(),
+            None => "import <nixpkgs>".to_string(),
         };
 
+        let overlays = plan
+            .nix_config
+            .overlays
+            .iter()
+            .map(|url| format!("(import (builtins.fetchTarball \"{}\"))", url))
+            .collect::<Vec<String>>()
+            .join("\n");
+
         let nix_expression = formatdoc! {"
-           {pkg_import} {{ }}; [ {pkgs} ]
+            {{ }}:
+
+            let
+              pkgs = {pkg_import} {{ 
+                overlays = [
+                  {overlays}
+                ];
+              }};
+            in with pkgs;
+            buildEnv {{
+              name = \"env\";
+              paths = [
+                {pkgs}
+              ];
+            }}
         ",
         pkg_import=pkg_import,
-        pkgs=nixpkgs};
+        pkgs=nixpkgs,
+        overlays=overlays};
 
         Ok(nix_expression)
     }
diff --git a/src/nixpacks/pkg.rs b/src/nixpacks/nix.rs
similarity index 66%
rename from src/nixpacks/pkg.rs
rename to src/nixpacks/nix.rs
index 71531ee..42faf33 100644
--- a/src/nixpacks/pkg.rs
+++ b/src/nixpacks/nix.rs
@@ -8,6 +8,13 @@ pub struct Pkg {
     pub overrides: HashMap<String, String>,
 }
 
+#[derive(Serialize, Deserialize, PartialEq, Clone, Default, Debug)]
+pub struct NixConfig {
+    pub pkgs: Vec<Pkg>,
+    pub overlays: Vec<String>,
+    pub nixpkgs_archive: Option<String>,
+}
+
 impl Pkg {
     pub fn new(name: &str) -> Pkg {
         Pkg {
@@ -36,6 +43,31 @@ impl Pkg {
     }
 }
 
+impl NixConfig {
+    pub fn new(pkgs: Vec<Pkg>) -> Self {
+        NixConfig {
+            pkgs,
+            overlays: Vec::new(),
+            nixpkgs_archive: None,
+        }
+    }
+
+    pub fn add_pkgs(mut self, new_pkgs: &mut Vec<Pkg>) -> Self {
+        self.pkgs.append(new_pkgs);
+        self
+    }
+
+    pub fn add_overlay(mut self, overlay: String) -> Self {
+        self.overlays.push(overlay);
+        self
+    }
+
+    pub fn set_archive(mut self, archive: String) -> Self {
+        self.nixpkgs_archive = Some(archive);
+        self
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/nixpacks/plan.rs b/src/nixpacks/plan.rs
index 28f99c6..9fa2f55 100644
--- a/src/nixpacks/plan.rs
+++ b/src/nixpacks/plan.rs
@@ -1,12 +1,11 @@
 use serde::{Deserialize, Serialize};
 
-use super::{environment::EnvironmentVariables, pkg::Pkg};
+use super::{environment::EnvironmentVariables, nix::NixConfig};
 
 #[derive(Debug, Serialize, Deserialize)]
 pub struct BuildPlan {
     pub version: String,
-    pub nixpkgs_archive: Option<String>,
-    pub pkgs: Vec<Pkg>,
+    pub nix_config: NixConfig,
     pub install_cmd: Option<String>,
     pub build_cmd: Option<String>,
     pub start_cmd: Option<String>,
diff --git a/src/providers/deno.rs b/src/providers/deno.rs
index 1cb31f8..4a482d7 100644
--- a/src/providers/deno.rs
+++ b/src/providers/deno.rs
@@ -1,5 +1,9 @@
 use super::Provider;
-use crate::nixpacks::{app::App, environment::Environment, pkg::Pkg};
+use crate::nixpacks::{
+    app::App,
+    environment::Environment,
+    nix::{NixConfig, Pkg},
+};
 use anyhow::Result;
 use regex::Regex;
 
@@ -15,8 +19,11 @@ impl Provider for DenoProvider {
         app.find_match(&re, "**/*.ts")
     }
 
-    fn pkgs(&self, _app: &App, _env: &Environment) -> Result<Vec<Pkg>> {
-        Ok(vec![Pkg::new("pkgs.stdenv"), Pkg::new("pkgs.deno")])
+    fn nix_config(&self, _app: &App, _env: &Environment) -> Result<NixConfig> {
+        Ok(NixConfig::new(vec![
+            Pkg::new("pkgs.stdenv"),
+            Pkg::new("pkgs.deno"),
+        ]))
     }
 
     fn install_cmd(&self, _app: &App, _env: &Environment) -> Result<Option<String>> {
diff --git a/src/providers/go.rs b/src/providers/go.rs
index 3153e84..ea88134 100644
--- a/src/providers/go.rs
+++ b/src/providers/go.rs
@@ -1,5 +1,9 @@
 use super::Provider;
-use crate::nixpacks::{app::App, environment::Environment, pkg::Pkg};
+use crate::nixpacks::{
+    app::App,
+    environment::Environment,
+    nix::{NixConfig, Pkg},
+};
 use anyhow::Result;
 
 pub struct GolangProvider {}
@@ -13,8 +17,11 @@ impl Provider for GolangProvider {
         Ok(app.includes_file("main.go"))
     }
 
-    fn pkgs(&self, _app: &App, _env: &Environment) -> Result<Vec<Pkg>> {
-        Ok(vec![Pkg::new("pkgs.stdenv"), Pkg::new("pkgs.go")])
+    fn nix_config(&self, _app: &App, _env: &Environment) -> Result<NixConfig> {
+        Ok(NixConfig::new(vec![
+            Pkg::new("pkgs.stdenv"),
+            Pkg::new("pkgs.go"),
+        ]))
     }
 
     fn install_cmd(&self, _app: &App, _env: &Environment) -> Result<Option<String>> {
diff --git a/src/providers/mod.rs b/src/providers/mod.rs
index 4ca3dbc..2831bc3 100644
--- a/src/providers/mod.rs
+++ b/src/providers/mod.rs
@@ -1,7 +1,7 @@
 use crate::nixpacks::{
     app::App,
     environment::{Environment, EnvironmentVariables},
-    pkg::Pkg,
+    nix::NixConfig,
 };
 use anyhow::Result;
 
@@ -14,7 +14,7 @@ pub mod yarn;
 pub trait Provider {
     fn name(&self) -> &str;
     fn detect(&self, app: &App, _env: &Environment) -> Result<bool>;
-    fn pkgs(&self, app: &App, _env: &Environment) -> Result<Vec<Pkg>>;
+    fn nix_config(&self, app: &App, _env: &Environment) -> Result<NixConfig>;
     fn install_cmd(&self, _app: &App, _env: &Environment) -> Result<Option<String>> {
         Ok(None)
     }
diff --git a/src/providers/npm.rs b/src/providers/npm.rs
index d33a319..2fbb861 100644
--- a/src/providers/npm.rs
+++ b/src/providers/npm.rs
@@ -4,17 +4,25 @@ use super::Provider;
 use crate::nixpacks::{
     app::App,
     environment::{Environment, EnvironmentVariables},
-    pkg::Pkg,
+    nix::{NixConfig, Pkg},
 };
 use anyhow::{bail, Result};
 use regex::Regex;
 use serde::{Deserialize, Serialize};
 
-pub struct NpmProvider {}
-
 const AVAILABLE_NODE_VERSIONS: &[u32] = &[10, 12, 14, 16, 17];
 const DEFAULT_NODE_PKG_NAME: &'static &str = &"pkgs.nodejs";
 
+#[derive(Serialize, Deserialize, Debug)]
+pub struct PackageJson {
+    pub name: String,
+    pub scripts: Option<HashMap<String, String>>,
+    pub engines: Option<HashMap<String, String>>,
+    pub main: Option<String>,
+}
+
+pub struct NpmProvider {}
+
 impl Provider for NpmProvider {
     fn name(&self) -> &str {
         "node"
@@ -24,11 +32,11 @@ impl Provider for NpmProvider {
         Ok(app.includes_file("package.json"))
     }
 
-    fn pkgs(&self, app: &App, _env: &Environment) -> Result<Vec<Pkg>> {
+    fn nix_config(&self, app: &App, _env: &Environment) -> Result<NixConfig> {
         let package_json: PackageJson = app.read_json("package.json")?;
         let node_pkg = NpmProvider::get_nix_node_pkg(&package_json)?;
 
-        Ok(vec![Pkg::new("pkgs.stdenv"), node_pkg])
+        Ok(NixConfig::new(vec![Pkg::new("pkgs.stdenv"), node_pkg]))
     }
 
     fn install_cmd(&self, _app: &App, _env: &Environment) -> Result<Option<String>> {
@@ -116,14 +124,6 @@ impl NpmProvider {
     }
 }
 
-#[derive(Serialize, Deserialize, Debug)]
-pub struct PackageJson {
-    pub name: String,
-    pub scripts: Option<HashMap<String, String>>,
-    pub engines: Option<HashMap<String, String>>,
-    pub main: Option<String>,
-}
-
 fn version_number_to_pkg(version: &u32) -> Result<Option<String>> {
     if AVAILABLE_NODE_VERSIONS.contains(version) {
         Ok(Some(format!("nodejs-{}_x", version)))
diff --git a/src/providers/rust.rs b/src/providers/rust.rs
index 3e3f68c..f638c1d 100644
--- a/src/providers/rust.rs
+++ b/src/providers/rust.rs
@@ -2,9 +2,26 @@ use super::Provider;
 use crate::nixpacks::{
     app::App,
     environment::{Environment, EnvironmentVariables},
-    pkg::Pkg,
+    nix::{NixConfig, Pkg},
 };
 use anyhow::{Context, Result};
+use serde::{Deserialize, Serialize};
+
+static RUST_OVERLAY: &str = "https://github.com/oxalica/rust-overlay/archive/master.tar.gz";
+static DEFAULT_RUST_PACKAGE: &str = "rust-bin.stable.latest.default";
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct CargoTomlPackage {
+    pub name: String,
+    pub version: String,
+    #[serde(rename = "rust-version")]
+    pub rust_version: Option<String>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct CargoToml {
+    pub package: CargoTomlPackage,
+}
 
 pub struct RustProvider {}
 
@@ -17,13 +34,15 @@ impl Provider for RustProvider {
         Ok(app.includes_file("Cargo.toml"))
     }
 
-    fn pkgs(&self, _app: &App, _env: &Environment) -> Result<Vec<Pkg>> {
-        Ok(vec![
+    fn nix_config(&self, app: &App, _env: &Environment) -> Result<NixConfig> {
+        let rust_pkg: Pkg = self.get_rust_pkg(app)?;
+
+        Ok(NixConfig::new(vec![
             Pkg::new("pkgs.stdenv"),
             Pkg::new("pkgs.gcc"),
-            Pkg::new("pkgs.rustc"),
-            Pkg::new("pkgs.cargo"),
+            rust_pkg,
         ])
+        .add_overlay(RUST_OVERLAY.to_string()))
     }
 
     fn install_cmd(&self, _app: &App, _env: &Environment) -> Result<Option<String>> {
@@ -35,18 +54,9 @@ impl Provider for RustProvider {
     }
 
     fn suggested_start_command(&self, app: &App, _env: &Environment) -> Result<Option<String>> {
-        if app.includes_file("Cargo.toml") {
-            // Parse name from Cargo.toml so we can run ./target/release/{name}
-            let toml_file: toml::Value =
-                app.read_toml("Cargo.toml").context("Reading Cargo.toml")?;
-            let name = toml_file
-                .get("package")
-                .and_then(|package| package.get("name"))
-                .and_then(|v| v.as_str());
-
-            if let Some(name) = name {
-                return Ok(Some(format!("./target/release/{}", name)));
-            }
+        if let Some(toml_file) = self.parse_cargo_toml(app)? {
+            let name = toml_file.package.name;
+            return Ok(Some(format!("./target/release/{}", name)));
         }
 
         Ok(None)
@@ -62,3 +72,33 @@ impl Provider for RustProvider {
         Ok(variables)
     }
 }
+
+impl RustProvider {
+    fn parse_cargo_toml(&self, app: &App) -> Result<Option<CargoToml>> {
+        if app.includes_file("Cargo.toml") {
+            let cargo_toml: CargoToml =
+                app.read_toml("Cargo.toml").context("Reading Cargo.toml")?;
+            return Ok(Some(cargo_toml));
+        }
+
+        Ok(None)
+    }
+
+    // Get the rust package version by parsing the `rust-version` field in `Cargo.toml`
+    fn get_rust_pkg(&self, app: &App) -> Result<Pkg> {
+        let pkg = match self.parse_cargo_toml(app)? {
+            Some(toml_file) => {
+                let version = toml_file.package.rust_version;
+
+                version
+                    .map(|version| {
+                        Pkg::new(format!("rust-bin.stable.\"{}\".default", version).as_str())
+                    })
+                    .unwrap_or_else(|| Pkg::new(DEFAULT_RUST_PACKAGE))
+            }
+            None => Pkg::new(DEFAULT_RUST_PACKAGE),
+        };
+
+        Ok(pkg)
+    }
+}
diff --git a/src/providers/yarn.rs b/src/providers/yarn.rs
index 9d88d5e..9ac3a7d 100644
--- a/src/providers/yarn.rs
+++ b/src/providers/yarn.rs
@@ -5,7 +5,7 @@ use super::{
 use crate::nixpacks::{
     app::App,
     environment::{Environment, EnvironmentVariables},
-    pkg::Pkg,
+    nix::{NixConfig, Pkg},
 };
 use anyhow::Result;
 
@@ -20,12 +20,12 @@ impl Provider for YarnProvider {
         Ok(app.includes_file("package.json") && app.includes_file("yarn.lock"))
     }
 
-    fn pkgs(&self, app: &App, _env: &Environment) -> Result<Vec<Pkg>> {
+    fn nix_config(&self, app: &App, _env: &Environment) -> Result<NixConfig> {
         let node_pkg = NpmProvider::get_nix_node_pkg(&app.read_json("package.json")?)?;
-        Ok(vec![
+        Ok(NixConfig::new(vec![
             Pkg::new("pkgs.stdenv"),
             Pkg::new("pkgs.yarn").set_override("nodejs", node_pkg.name.as_str()),
-        ])
+        ]))
     }
 
     fn install_cmd(&self, _app: &App, _env: &Environment) -> Result<Option<String>> {


Test patch:
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index 953cb45..0a9d49f 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -1,5 +1,5 @@
 use anyhow::Result;
-use nixpacks::{gen_plan, nixpacks::pkg::Pkg};
+use nixpacks::{gen_plan, nixpacks::nix::Pkg};
 
 #[test]
 fn test_node() -> Result<()> {
@@ -54,7 +54,7 @@ fn test_node_custom_version() -> Result<()> {
         false,
     )?;
     assert_eq!(
-        plan.pkgs,
+        plan.nix_config.pkgs,
         vec![Pkg::new("pkgs.stdenv"), Pkg::new("nodejs-12_x")]
     );
 
@@ -89,7 +89,7 @@ fn test_yarn_custom_version() -> Result<()> {
         false,
     )?;
     assert_eq!(
-        plan.pkgs,
+        plan.nix_config.pkgs,
         vec![
             Pkg::new("pkgs.stdenv"),
             Pkg::new("pkgs.yarn").set_override("nodejs", "nodejs-14_x")
@@ -145,7 +145,7 @@ fn test_custom_pkgs() -> Result<()> {
         Vec::new(),
         false,
     )?;
-    assert_eq!(plan.pkgs, vec![Pkg::new("cowsay")]);
+    assert_eq!(plan.nix_config.pkgs, vec![Pkg::new("cowsay")]);
     assert_eq!(plan.start_cmd, Some("./start.sh".to_string()));
 
     Ok(())
@@ -154,7 +154,31 @@ fn test_custom_pkgs() -> Result<()> {
 #[test]
 fn test_pin_archive() -> Result<()> {
     let plan = gen_plan("./examples/hello", Vec::new(), None, None, Vec::new(), true)?;
-    assert!(plan.nixpkgs_archive.is_some());
+    assert!(plan.nix_config.nixpkgs_archive.is_some());
+
+    Ok(())
+}
+
+#[test]
+fn test_custom_rust_version() -> Result<()> {
+    let plan = gen_plan(
+        "./examples/rust-custom-version",
+        Vec::new(),
+        None,
+        None,
+        Vec::new(),
+        false,
+    )?;
+    assert_eq!(plan.build_cmd, Some("cargo build --release".to_string()));
+    assert_eq!(
+        plan.nix_config
+            .pkgs
+            .iter()
+            .filter(|p| p.name.contains("1.56.0"))
+            .count(),
+        1
+    );
+    assert!(!plan.nix_config.overlays.is_empty());
 
     Ok(())
 }