Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pixi build cli #2117

Merged
Merged
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
32 changes: 26 additions & 6 deletions crates/pixi_build_types/src/procedures/conda_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ pub struct CondaBuildParams {
/// The channel configuration to use to resolve dependencies.
pub channel_configuration: ChannelConfiguration,

/// Information about the output to build. This information is previously
/// returned from a call to `conda/getMetadata`.
/// Information about the outputs to build. This information is previously
/// returned from a call to `conda/getMetadata`. Pass `None` to build all
/// outputs.
#[serde(default)]
pub output: CondaOutputIdentifier,
pub outputs: Option<Vec<CondaOutputIdentifier>>,
}

/// Identifier of an output.
Expand All @@ -39,9 +40,28 @@ pub struct CondaOutputIdentifier {
/// Contains the result of the `conda/build` request.
#[derive(Debug, Serialize, Deserialize)]
pub struct CondaBuildResult {
// TODO: Should this be a UTF8 encoded type
/// The packages that were built.
pub packages: Vec<CondaBuiltPackage>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CondaBuiltPackage {
/// The location on disk where the built package is located.
pub output_file: PathBuf,
/// The globs that were used as input to the build.
/// use these for re-verifying the build.

/// The globs that were used as input to the build. Use these for
/// re-verifying the build.
pub input_globs: Vec<String>,

/// The name of the package.
pub name: String,

/// The version of the package.
pub version: String,

/// The build string of the package.
pub build: String,

/// The subdirectory of the package.
pub subdir: String,
}
9 changes: 7 additions & 2 deletions src/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,21 +146,26 @@ impl BuildContext {
channel_configuration: ChannelConfiguration {
base_url: self.channel_config.channel_alias.clone(),
},
output: CondaOutputIdentifier {
outputs: Some(vec![CondaOutputIdentifier {
name: Some(source_spec.package_record.name.as_normalized().to_string()),
version: Some(source_spec.package_record.version.version().to_string()),
build: Some(source_spec.package_record.build.clone()),
subdir: Some(source_spec.package_record.subdir.clone()),
},
}]),
})
.await
.map_err(|e| BuildError::BackendError(e.into()))?;

let build_result = build_result.packages.into_iter().next().ok_or_else(|| {
BuildError::FrontendError(miette::miette!("no packages were built").into())
})?;

// Add the sha256 to the package record.
let sha = rattler_digest::compute_file_digest::<Sha256>(&build_result.output_file)
.map_err(|e| BuildError::CalculateSha(source, e))?;
let mut package_record = source_spec.package_record.clone();
package_record.sha256 = Some(sha);

// Construct a repodata record that represents the package
let record = RepoDataRecord {
package_record,
Expand Down
133 changes: 133 additions & 0 deletions src/cli/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use std::path::PathBuf;

use clap::Parser;
use miette::{Context, IntoDiagnostic};
use pixi_build_frontend::{BackendOverrides, SetupRequest};
use pixi_build_types::{procedures::conda_build::CondaBuildParams, ChannelConfiguration};
use pixi_config::ConfigCli;
use pixi_manifest::FeaturesExt;
use rattler_conda_types::Platform;

use crate::{cli::cli_config::ProjectConfig, Project};

#[derive(Parser, Debug)]
#[clap(verbatim_doc_comment)]
pub struct Args {
#[clap(flatten)]
pub project_config: ProjectConfig,

#[clap(flatten)]
pub config_cli: ConfigCli,

/// The target platform to build for (defaults to the current platform)
#[clap(long, short, default_value_t = Platform::current())]
pub target_platform: Platform,

/// The output directory to place the build artifacts
#[clap(long, short, default_value = ".")]
pub output_dir: PathBuf,
}

#[cfg(unix)]
const EXDEV: i32 = 18;

#[cfg(windows)]
const EXDEV: i32 = 17;

pub async fn execute(args: Args) -> miette::Result<()> {
let project = Project::load_or_else_discover(args.project_config.manifest_path.as_deref())?
.with_cli_config(args.config_cli);

// TODO: Implement logic to take the source code from a VCS instead of from a
// local channel so that that information is also encoded in the manifest.

// Instantiate a protocol for the source directory.
let channel_config = project.channel_config();
let protocol = pixi_build_frontend::BuildFrontend::default()
.with_channel_config(channel_config.clone())
.setup_protocol(SetupRequest {
source_dir: project.root().to_path_buf(),
build_tool_overrides: BackendOverrides {
spec: None,
path: Some("pixi-build-python".into()),
},
})
.await
.into_diagnostic()?;

// Build the individual packages.
let result = protocol
.conda_build(&CondaBuildParams {
target_platform: Some(args.target_platform),
channel_base_urls: Some(
project
.default_environment()
.channels()
.iter()
.map(|&c| c.clone().into_base_url(&channel_config))
.collect(),
),
channel_configuration: ChannelConfiguration {
base_url: channel_config.channel_alias,
},
outputs: None,
})
.await?;

// Move the built packages to the output directory.
let output_dir = args.output_dir;
for package in result.packages {
std::fs::create_dir_all(&output_dir)
.into_diagnostic()
.with_context(|| {
format!(
"failed to create output directory '{0}'",
output_dir.display()
)
})?;

let file_name = package.output_file.file_name().ok_or_else(|| {
miette::miette!(
"output file '{0}' does not have a file name",
package.output_file.display()
)
})?;
let dest = output_dir.join(file_name);
match std::fs::rename(&package.output_file, &dest) {
Ok(_) => {
println!(
"{}Successfully built '{}'",
console::style(console::Emoji("✔ ", "")).green(),
dest.display()
);
}
Err(e) if e.raw_os_error() == Some(EXDEV) => {
std::fs::copy(&package.output_file, &dest)
.into_diagnostic()
.with_context(|| {
format!(
"failed to copy {} to {}",
package.output_file.display(),
dest.display()
)
})?;
if let Err(e) = std::fs::remove_file(&package.output_file) {
tracing::warn!(
"failed to remove {} after copying it to the output directory: {}",
package.output_file.display(),
e
);
}
}
Err(e) => Err(e).into_diagnostic().with_context(|| {
format!(
"failed to move {} to {}",
package.output_file.display(),
dest.display()
)
})?,
}
}

Ok(())
}
15 changes: 9 additions & 6 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ use clap::Parser;
use clap_verbosity_flag::Verbosity;
use indicatif::ProgressDrawTarget;
use miette::IntoDiagnostic;
use pixi_progress::{self, global_multi_progress};
use pixi_utils::indicatif::IndicatifWriter;
use tracing_subscriber::{
filter::LevelFilter, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt,
EnvFilter,
};

use pixi_progress;
use pixi_progress::global_multi_progress;
use pixi_utils::indicatif::IndicatifWriter;

pub mod add;
mod build;
pub mod clean;
pub mod cli_config;
pub mod completion;
Expand Down Expand Up @@ -126,14 +125,17 @@ pub enum Command {
SelfUpdate(self_update::Args),
Clean(clean::Args),
Completion(completion::Args),

// Build
Build(build::Args),
}

#[derive(Parser, Debug, Default, Copy, Clone)]
#[group(multiple = false)]
/// Lock file usage from the CLI
pub struct LockFileUsageArgs {
/// Install the environment as defined in the lockfile, doesn't update lockfile if it isn't
/// up-to-date with the manifest file.
/// Install the environment as defined in the lockfile, doesn't update
/// lockfile if it isn't up-to-date with the manifest file.
#[clap(long, conflicts_with = "locked", env = "PIXI_FROZEN")]
pub frozen: bool,
/// Check if lockfile is up-to-date before installing the environment,
Expand Down Expand Up @@ -277,6 +279,7 @@ pub async fn execute_command(command: Command) -> miette::Result<()> {
Command::Tree(cmd) => tree::execute(cmd).await,
Command::Update(cmd) => update::execute(cmd).await,
Command::Exec(args) => exec::execute(args).await,
Command::Build(args) => build::execute(args).await,
}
}

Expand Down
Loading