Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ site
.pixi
target-pixi
*.conda

# Test files generated during pyproject recipe development
*example*.toml
*recipe*.yaml
/recipe/
59 changes: 59 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ Build a package from a recipe
Variant configuration files for the build


- `--variant <VARIANT_OVERRIDES>`

Override specific variant values (e.g. --variant python=3.12 or --variant python=3.12,3.11). Multiple values separated by commas will create multiple build variants


- `--ignore-recipe-variants`

Do not read the `variants.yaml` file next to a recipe
Expand Down Expand Up @@ -684,6 +689,7 @@ Generate a recipe from PyPI, CRAN, CPAN, or LuaRocks
* `cran` β€” Generate a recipe for an R package from CRAN
* `cpan` β€” Generate a recipe for a Perl package from CPAN
* `luarocks` β€” Generate a recipe for a Lua package from LuaRocks
* `pyproject` β€” Generate a recipe from a local pyproject.toml file



Expand Down Expand Up @@ -819,6 +825,59 @@ Generate a recipe for a Lua package from LuaRocks



#### `pyproject`

Generate a recipe from a local pyproject.toml file

**Usage:** `rattler-build generate-recipe pyproject [OPTIONS]`

##### **Options:**

- `-i`, `--input <INPUT>`

Path to the pyproject.toml file (defaults to pyproject.toml in current directory)

- Default value: `pyproject.toml`

- `-o`, `--output <OUTPUT>`

Path to write the recipe.yaml file. If not provided, output will be printed to stdout


- `--overwrite`

Whether to overwrite existing recipe file


- `--format <FORMAT>`

Output format: yaml or json

- Default value: `yaml`

- `--sort-keys`

Sort keys in output


- `--include-comments`

Include helpful comments in the output


- `--exclude-sections <EXCLUDE_SECTIONS>`

Exclude specific sections from the output (comma-separated)


- `--validate`

Validate the generated recipe





### `auth`

Handle authentication to external channels
Expand Down
190 changes: 142 additions & 48 deletions pixi.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,3 @@ channels = [
[package]
name = "rattler-build"
version = "0.0.0dev" # NOTE: how to set this automatically?

144 changes: 144 additions & 0 deletions rust-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,25 @@ mod tests {

output
}

fn generate_recipe_pyproject<I: AsRef<Path>, O: AsRef<Path>>(
&self,
input: I,
output: O,
) -> Output {
let input_str = input.as_ref().display().to_string();
let output_str = output.as_ref().display().to_string();
let args = vec![
"--log-style=plain",
"generate-recipe",
"pyproject",
"-i",
input_str.as_str(),
"-o",
output_str.as_str(),
];
self.with_args(args)
}
}

#[allow(unreachable_code)]
Expand Down Expand Up @@ -715,4 +734,129 @@ requirements:
assert!(output.contains("No license files were copied"));
assert!(output.contains("The following license files were not found: *.license"));
}

#[test]
fn test_generate_recipe_pyproject_basic() {
let tmp = tmp("test_generate_recipe_pyproject_basic");
// Create the temp directory
fs::create_dir_all(tmp.as_dir()).unwrap();

// Create a basic pyproject.toml
let pyproject_content = r#"
[project]
name = "test-package"
version = "1.0.0"
description = "A test package for pyproject recipe generation"
dependencies = [
"requests>=2.25.0",
"click>=8.0.0"
]

[project.scripts]
test-tool = "test_package.cli:main"

[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
"#;

let pyproject_path = tmp.as_dir().join("pyproject.toml");
fs::write(&pyproject_path, pyproject_content).unwrap();

let recipe_path = tmp.as_dir().join("recipe.yaml");

// Run rattler-build generate-recipe pyproject
let rattler_build = rattler().generate_recipe_pyproject(&pyproject_path, &recipe_path);

assert!(
rattler_build.status.success(),
"Command failed: {}",
String::from_utf8_lossy(&rattler_build.stdout)
);

// Check that recipe.yaml was created
assert!(recipe_path.exists(), "recipe.yaml was not created");

// Check recipe content
let recipe_content = fs::read_to_string(&recipe_path).unwrap();

// Should have schema header
assert!(recipe_content.contains("# yaml-language-server: $schema="));
assert!(recipe_content.contains("schema_version: 1"));

// Should have correct package info
assert!(recipe_content.contains("name: test-package"));
assert!(recipe_content.contains("version: 1.0.0"));

// Should have dependencies converted
assert!(recipe_content.contains("requests >=2.25.0"));
assert!(recipe_content.contains("click >=8.0.0"));

// Should have entry points
assert!(recipe_content.contains("test-tool = test_package.cli:main"));

// Should have build system requirements
assert!(recipe_content.contains("setuptools"));
assert!(recipe_content.contains("wheel"));
}

#[test]
fn test_generate_recipe_pyproject_with_conda_overrides() {
let tmp = tmp("test_generate_recipe_pyproject_conda_overrides");
// Create the temp directory
fs::create_dir_all(tmp.as_dir()).unwrap();

// Create a pyproject.toml with conda recipe overrides
let pyproject_content = r#"
[project]
name = "advanced-package"
version = "2.0.0"
description = "An advanced package with conda overrides"
dependencies = [
"numpy>=1.21.0"
]

[tool.conda.recipe]
schema_version = 2

[tool.conda.recipe.context]
custom_var = "custom_value"

[tool.conda.recipe.about]
license = "MIT"
homepage = "https://example.com"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#;

let pyproject_path = tmp.as_dir().join("pyproject.toml");
fs::write(&pyproject_path, pyproject_content).unwrap();

let recipe_path = tmp.as_dir().join("recipe.yaml");

// Run rattler-build generate-recipe pyproject
let rattler_build = rattler().generate_recipe_pyproject(&pyproject_path, &recipe_path);

assert!(
rattler_build.status.success(),
"Command failed: {}",
String::from_utf8_lossy(&rattler_build.stdout)
);

// Check recipe content
let recipe_content = fs::read_to_string(&recipe_path).unwrap();

// Should have custom schema version
assert!(recipe_content.contains("schema_version: 2"));

// Should have conda overrides applied
assert!(recipe_content.contains("custom_var: custom_value"));
assert!(recipe_content.contains("license: MIT"));
assert!(recipe_content.contains("homepage: https://example.com"));

// Should have hatchling build system
assert!(recipe_content.contains("hatchling"));
}
}
2 changes: 2 additions & 0 deletions src/recipe_generator/luarocks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ fn rockspec_to_recipe(rockspec: &LuarocksRockspec) -> miette::Result<Recipe> {
};

let mut recipe = Recipe {
schema_version: Some(1),
context,
package: crate::recipe_generator::serialize::Package {
name: package_name.as_normalized().to_string(),
Expand All @@ -388,6 +389,7 @@ fn rockspec_to_recipe(rockspec: &LuarocksRockspec) -> miette::Result<Recipe> {
source: vec![source_element],
build: Build {
script: "# Take the first `rockspec` we find (in non-deterministic places unfortunately)\nROCK=$(find . -name \"*.rockspec\" | sort -n -r | head -n 1)\nluarocks install ${ROCK} --tree=${{ PREFIX }}".to_string(),
number: Some(0),
python: Python::default(),
noarch: None,
},
Expand Down
8 changes: 7 additions & 1 deletion src/recipe_generator/mod.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
//! Module for generating recipes for Python (PyPI), R (CRAN), Perl (CPAN), or Lua (LuaRocks) packages
//! Module for generating recipes for Python (PyPI), R (CRAN), Perl (CPAN), Lua (LuaRocks) packages, or local Python projects
use clap::Parser;

mod cpan;
mod cran;
mod luarocks;
mod pypi;
mod pyproject;
mod serialize;

use cpan::{CpanOpts, generate_cpan_recipe};
use cran::{CranOpts, generate_r_recipe};
use luarocks::{LuarocksOpts, generate_luarocks_recipe};
use pypi::PyPIOpts;
use pyproject::{PyprojectOpts, generate_pyproject_recipe};
pub use serialize::write_recipe;

use self::pypi::generate_pypi_recipe;
Expand All @@ -29,6 +31,9 @@ pub enum Source {

/// Generate a recipe for a Lua package from LuaRocks
Luarocks(LuarocksOpts),

/// Generate a recipe from a local pyproject.toml file
Pyproject(PyprojectOpts),
}

/// Options for generating a recipe
Expand All @@ -46,6 +51,7 @@ pub async fn generate_recipe(args: GenerateRecipeOpts) -> miette::Result<()> {
Source::Cran(opts) => generate_r_recipe(&opts).await?,
Source::Cpan(opts) => generate_cpan_recipe(&opts).await?,
Source::Luarocks(opts) => generate_luarocks_recipe(&opts).await?,
Source::Pyproject(opts) => generate_pyproject_recipe(&opts).await?,
}

Ok(())
Expand Down
Loading
Loading