Repository: railwayapp/nixpacks
Pull request #334 (2022-07-19 19:45:30)

Fix rust compilation of `openssl-sys`
## What does this PR address?
<!-- Thanks for sending a pull request! -->
Fix building of Rust projects that use the `openssl-sys` trait. A few things were needed
- Do not build for the MUSL target if openssl is detected
- Set the `OPENSSL_DIR` and `OPENSSL_LIB_DIR` environment variables to the correct locations

The solution mainly comes from a few resources
- https://github.com/PierreZ/nix-env-dev/blob/9dcb33911361b3184af072797baffd340beb0ef7/rust.nix
- https://discourse.nixos.org/t/rust-openssl-woes/12340/3
- https://discourse.nixos.org/t/openssl-dependency-for-rust/3186/3

The `environment.nix` file generation has custom implementation to handle this when the `openssl` Nix library is detected. Ideally this is made generic in the future where providers can set variables based on Nix package locations. But I will think about what that API looks like and possibly add in https://github.com/railwayapp/nixpacks/pull/300. For now this works.

To parse the `Cargo.toml` file in a more accurate way, I am using the `cargo_toml` crate.

<!-- Remove if not applicable -->
Fixes https://github.com/railwayapp/nixpacks/issues/286




Patch:
diff --git a/Cargo.lock b/Cargo.lock
index cd96827..88b0266 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -61,6 +61,17 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "cargo_toml"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5809dd3e6444651fd1cdd3dbec71eca438c439a0fcc8081674a14da0afe50185"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "toml",
+]
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
@@ -304,6 +315,7 @@ dependencies = [
  "anyhow",
  "base64",
  "box_drawing",
+ "cargo_toml",
  "clap",
  "colored",
  "console",
diff --git a/Cargo.toml b/Cargo.toml
index 4bb3887..ed3f680 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,3 +41,4 @@ base64 = "0.13.0"
 console = "0.15.0"
 box_drawing = "0.1.2"
 textwrap = "0.15.0"
+cargo_toml = "0.11.5"
diff --git a/examples/rust-openssl/.gitignore b/examples/rust-openssl/.gitignore
new file mode 100644
index 0000000..8c88100
--- /dev/null
+++ b/examples/rust-openssl/.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-openssl/Cargo.toml b/examples/rust-openssl/Cargo.toml
new file mode 100644
index 0000000..cd93b1d
--- /dev/null
+++ b/examples/rust-openssl/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "rust-open-ssl"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+openssl = "0.10.41"
+openssl-sys = "0.9.75"
diff --git a/examples/rust-openssl/src/main.rs b/examples/rust-openssl/src/main.rs
new file mode 100644
index 0000000..aa7de27
--- /dev/null
+++ b/examples/rust-openssl/src/main.rs
@@ -0,0 +1,5 @@
+use openssl as _;
+
+fn main() {
+    println!("Hello from Rust openssl!")
+}
diff --git a/src/nixpacks/nix/mod.rs b/src/nixpacks/nix/mod.rs
index 638c0c9..1463427 100644
--- a/src/nixpacks/nix/mod.rs
+++ b/src/nixpacks/nix/mod.rs
@@ -37,6 +37,17 @@ pub fn create_nix_expression(plan: &BuildPlan) -> String {
         .collect::<Vec<String>>()
         .join("\n");
 
+    // If the openssl library is added, set the OPENSSL_DIR and OPENSSL_LIB_DIR environment variables
+    // In the future, we will probably want a generic way for providers to set variables based off Nix package locations
+    let openssl_dirs = if libraries.contains("openssl") {
+        formatdoc! {"
+      export OPENSSL_DIR=\"${{openssl.dev}}\"
+      export OPENSSL_LIB_DIR=\"${{openssl.out}}/lib\"
+    "}
+    } else {
+        String::new()
+    };
+
     let nix_expression = formatdoc! {"
             {{ }}:
 
@@ -46,6 +57,7 @@ pub fn create_nix_expression(plan: &BuildPlan) -> String {
                 APPEND_LIBRARY_PATH = \"${{lib.makeLibraryPath [ {libraries} ] }}\";
                 myLibraries = writeText \"libraries\" ''
                   export LD_LIBRARY_PATH=\"${{APPEND_LIBRARY_PATH}}:$LD_LIBRARY_PATH\"
+                  {openssl_dirs}
                 '';
               in
                 buildEnv {{
diff --git a/src/providers/python.rs b/src/providers/python.rs
index 279d3ea..3de1359 100644
--- a/src/providers/python.rs
+++ b/src/providers/python.rs
@@ -381,14 +381,12 @@ mod test {
 
     #[test]
     fn test_numpy_detection() -> Result<()> {
-        assert_eq!(
-            PythonProvider::uses_numpy(&App::new("./examples/python")?)?,
-            false
-        );
-        assert_eq!(
-            PythonProvider::uses_numpy(&App::new("./examples/python-numpy")?)?,
-            true
-        );
+        assert!(!PythonProvider::uses_numpy(&App::new(
+            "./examples/python"
+        )?)?,);
+        assert!(PythonProvider::uses_numpy(&App::new(
+            "./examples/python-numpy"
+        )?)?,);
         Ok(())
     }
 }
diff --git a/src/providers/rust.rs b/src/providers/rust.rs
index cba6d7f..05ec43a 100644
--- a/src/providers/rust.rs
+++ b/src/providers/rust.rs
@@ -8,7 +8,7 @@ use crate::nixpacks::{
     phase::{BuildPhase, SetupPhase, StartPhase},
 };
 use anyhow::{Context, Result};
-use serde::{Deserialize, Serialize};
+use cargo_toml::Manifest;
 
 static RUST_OVERLAY: &str = "https://github.com/oxalica/rust-overlay/archive/master.tar.gz";
 static DEFAULT_RUST_PACKAGE: &str = "rust-bin.stable.latest.default";
@@ -17,19 +17,6 @@ const CARGO_GIT_CACHE_DIR: &'static &str = &"/root/.cargo/git";
 const CARGO_REGISTRY_CACHE_DIR: &'static &str = &"/root/.cargo/registry";
 const CARGO_TARGET_CACHE_DIR: &'static &str = &"target";
 
-#[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 {}
 
 impl Provider for RustProvider {
@@ -56,7 +43,12 @@ impl Provider for RustProvider {
             setup_phase.add_file_dependency(toolchain_file);
         }
 
-        if !env.is_config_variable_truthy("NO_MUSL") {
+        // Custom libs for openssl
+        if RustProvider::uses_openssl(app)? {
+            setup_phase.add_libraries(vec!["openssl".to_string(), "openssl.dev".to_string()]);
+        }
+
+        if RustProvider::should_use_musl(app, env)? {
             setup_phase.add_apt_pkgs(vec!["musl-tools".to_string()]);
         }
 
@@ -136,26 +128,26 @@ impl Provider for RustProvider {
 impl RustProvider {
     fn get_app_name(app: &App) -> Result<Option<String>> {
         if let Some(toml_file) = RustProvider::parse_cargo_toml(app)? {
-            let name = toml_file.package.name;
-            Ok(Some(name))
-        } else {
-            Ok(None)
+            if let Some(package) = toml_file.package {
+                let name = package.name;
+                return Ok(Some(name));
+            }
         }
+
+        Ok(None)
     }
 
-    fn get_target(_app: &App, env: &Environment) -> Result<Option<String>> {
-        // All the user to use the default target instead of compiling with musl
-        if !env.is_config_variable_truthy("NO_MUSL") {
+    fn get_target(app: &App, env: &Environment) -> Result<Option<String>> {
+        if RustProvider::should_use_musl(app, env)? {
             Ok(Some(format!("{}-unknown-linux-musl", ARCH)))
         } else {
             Ok(None)
         }
     }
 
-    fn parse_cargo_toml(app: &App) -> Result<Option<CargoToml>> {
+    fn parse_cargo_toml(app: &App) -> Result<Option<Manifest>> {
         if app.includes_file("Cargo.toml") {
-            let cargo_toml: CargoToml =
-                app.read_toml("Cargo.toml").context("Reading Cargo.toml")?;
+            let cargo_toml: Manifest = app.read_toml("Cargo.toml").context("Reading Cargo.toml")?;
             return Ok(Some(cargo_toml));
         }
 
@@ -187,20 +179,58 @@ impl RustProvider {
         }
 
         let pkg = match RustProvider::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))
-            }
+            Some(toml_file) => toml_file
+                .package
+                .map(|package| {
+                    package
+                        .rust_version
+                        .map(|version| {
+                            Pkg::new(format!("rust-bin.stable.\"{}\".default", version).as_str())
+                        })
+                        .unwrap_or_else(|| Pkg::new(DEFAULT_RUST_PACKAGE))
+                })
+                .unwrap_or_else(|| Pkg::new(DEFAULT_RUST_PACKAGE)),
             None => Pkg::new(DEFAULT_RUST_PACKAGE),
         };
 
         Ok(pkg)
     }
+
+    fn should_use_musl(app: &App, env: &Environment) -> Result<bool> {
+        if env.is_config_variable_truthy("NO_MUSL") {
+            return Ok(false);
+        }
+
+        if RustProvider::get_rust_toolchain_file(app)?.is_some() {
+            return Ok(false);
+        }
+
+        // Do not build for the musl target if using openssl
+        if RustProvider::uses_openssl(app)? {
+            return Ok(false);
+        }
+
+        Ok(true)
+    }
+
+    fn uses_openssl(app: &App) -> Result<bool> {
+        // Check Cargo.toml
+        if let Some(toml_file) = RustProvider::parse_cargo_toml(app)? {
+            if toml_file.dependencies.contains_key("openssl")
+                || toml_file.dev_dependencies.contains_key("openssl")
+                || toml_file.build_dependencies.contains_key("openssl")
+            {
+                return Ok(true);
+            }
+        }
+
+        // Check Cargo.lock
+        if app.includes_file("Cargo.lock") && app.read_file("Cargo.lock")?.contains("openssl") {
+            return Ok(true);
+        }
+
+        Ok(false)
+    }
 }
 
 #[cfg(test)]
@@ -262,4 +292,16 @@ mod test {
 
         Ok(())
     }
+
+    #[test]
+    fn test_uses_openssl() -> Result<()> {
+        assert!(!RustProvider::uses_openssl(&App::new(
+            "./examples/rust-custom-version"
+        )?)?,);
+        assert!(RustProvider::uses_openssl(&App::new(
+            "./examples/rust-openssl"
+        )?)?,);
+
+        Ok(())
+    }
 }


Test patch:
diff --git a/tests/docker_run_tests.rs b/tests/docker_run_tests.rs
index 6c7c884..936ba81 100644
--- a/tests/docker_run_tests.rs
+++ b/tests/docker_run_tests.rs
@@ -394,6 +394,13 @@ fn test_rust_ring() {
     assert!(output.contains("Hello from rust"));
 }
 
+#[test]
+fn test_rust_openssl() {
+    let name = simple_build("./examples/rust-openssl");
+    let output = run_image(name, None);
+    assert!(output.contains("Hello from Rust openssl!"));
+}
+
 #[test]
 fn test_go() {
     let name = simple_build("./examples/go");