From 33ea124b94fff84acccf849133f0dfb153562577 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Tue, 4 Nov 2025 07:53:10 +0100 Subject: [PATCH] feat: add pixi-exec conda-script support --- crates/pixi_cli/src/exec.rs | 169 +++++++- crates/pixi_manifest/src/lib.rs | 1 + crates/pixi_manifest/src/script_metadata.rs | 419 ++++++++++++++++++++ examples/conda-script/README.md | 81 ++++ examples/conda-script/hello_python.py | 25 ++ examples/conda-script/platform_specific.py | 73 ++++ examples/conda-script/web_request.py | 39 ++ 7 files changed, 791 insertions(+), 16 deletions(-) create mode 100644 crates/pixi_manifest/src/script_metadata.rs create mode 100644 examples/conda-script/README.md create mode 100644 examples/conda-script/hello_python.py create mode 100644 examples/conda-script/platform_specific.py create mode 100644 examples/conda-script/web_request.py diff --git a/crates/pixi_cli/src/exec.rs b/crates/pixi_cli/src/exec.rs index bf2ce278b6..88ac051b30 100644 --- a/crates/pixi_cli/src/exec.rs +++ b/crates/pixi_cli/src/exec.rs @@ -1,10 +1,17 @@ -use std::{collections::BTreeSet, collections::HashMap, path::Path, str::FromStr, sync::LazyLock}; +use std::{ + collections::BTreeSet, + collections::HashMap, + path::{Path, PathBuf}, + str::FromStr, + sync::LazyLock, +}; use clap::{Parser, ValueHint}; use itertools::Itertools; use miette::{Context, IntoDiagnostic}; use pixi_config::{self, Config, ConfigCli}; use pixi_core::environment::list::{PackageToOutput, print_package_table}; +use pixi_manifest::script_metadata::{ScriptMetadata, ScriptMetadataError}; use pixi_progress::{await_in_progress, global_multi_progress, wrap_in_progress}; use pixi_utils::prefix::Prefix; use pixi_utils::{AsyncPrefixGuard, EnvironmentHash, reqwest::build_reqwest_clients}; @@ -12,7 +19,7 @@ use rattler::{ install::{IndicatifReporter, Installer}, package_cache::PackageCache, }; -use rattler_conda_types::{GenericVirtualPackage, MatchSpec, PackageName, Platform}; +use rattler_conda_types::{Channel, GenericVirtualPackage, MatchSpec, PackageName, Platform}; use rattler_solve::{SolverImpl, SolverTask, resolvo::Solver}; use rattler_virtual_packages::{VirtualPackageOverrides, VirtualPackages}; use reqwest_middleware::ClientWithMiddleware; @@ -74,14 +81,92 @@ pub async fn execute(args: Args) -> miette::Result<()> { let command = command_iter.next().ok_or_else(|| miette::miette!(help ="i.e when specifying specs explicitly use a command at the end: `pixi exec -s python==3.12 python`", "missing required command to execute",))?; let (_, client) = build_reqwest_clients(Some(&config), None)?; + // Check if the first argument is a script file with embedded metadata + let script_path = PathBuf::from(command); + let script_metadata = if script_path.exists() && script_path.is_file() { + match ScriptMetadata::from_file(&script_path) { + Ok(metadata) => { + tracing::info!("Found conda-script metadata in {}", script_path.display()); + Some(metadata) + } + Err(ScriptMetadataError::NoMetadataFound) => { + tracing::debug!( + "No conda-script metadata found in {}", + script_path.display() + ); + None + } + Err(e) => { + return Err(miette::miette!( + "Failed to parse conda-script metadata from {}: {}", + script_path.display(), + e + )); + } + } + } else { + None + }; + // Determine the specs for installation and for the environment name. let mut name_specs = args.specs.clone(); name_specs.extend(args.with.clone()); let mut install_specs = name_specs.clone(); + let mut channels_from_metadata: Option> = None; + let mut entrypoint_from_metadata: Option = None; + + // If we have script metadata, use it to augment or replace the specs and channels + if let Some(ref metadata) = script_metadata { + if name_specs.is_empty() { + // If no specs were provided via CLI, use the ones from metadata + let deps = metadata + .get_dependencies(args.platform) + .into_diagnostic() + .context("failed to get dependencies from script metadata")?; + install_specs = deps.clone(); + name_specs = deps; + tracing::info!( + "Using {} dependencies from script metadata", + name_specs.len() + ); + } else { + tracing::debug!("CLI specs provided, ignoring dependencies from script metadata"); + } + + // Get channels from metadata and convert to Channel type + // We always get channels from metadata when available, and pass them down + // The actual decision of whether to use them is made in create_exec_prefix + let named_channels = metadata + .get_channels() + .into_diagnostic() + .context("failed to get channels from script metadata")?; + + // Convert NamedChannelOrUrl to Channel + let channels: Result, _> = named_channels + .iter() + .map(|nc| nc.clone().into_channel(&config.global_channel_config())) + .collect(); + channels_from_metadata = Some( + channels + .into_diagnostic() + .context("failed to parse channels from metadata")?, + ); + tracing::info!( + "Using {} channels from script metadata", + channels_from_metadata.as_ref().unwrap().len() + ); + + // Get entrypoint from metadata + entrypoint_from_metadata = metadata.get_entrypoint(args.platform); + if let Some(ref ep) = entrypoint_from_metadata { + tracing::info!("Using entrypoint from script metadata: {}", ep); + } + } // Guess a package from the command if no specs were provided at all OR if --with is used - let should_guess_package = name_specs.is_empty() || !args.with.is_empty(); + let should_guess_package = + (name_specs.is_empty() || !args.with.is_empty()) && script_metadata.is_none(); if should_guess_package { install_specs.push(guess_package_spec(command)); } @@ -94,6 +179,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { &config, &client, should_guess_package, + channels_from_metadata.as_deref(), ) .await?; @@ -129,10 +215,41 @@ pub async fn execute(args: Args) -> miette::Result<()> { // Ignore CTRL+C so that the child is responsible for its own signal handling. let _ctrl_c = tokio::spawn(async { while tokio::signal::ctrl_c().await.is_ok() {} }); + // Determine the command to run - use entrypoint from metadata if available + let (actual_command, actual_args) = if let Some(ref entrypoint) = entrypoint_from_metadata { + // Replace ${SCRIPT} with the script path in the entrypoint + let expanded_entrypoint = entrypoint.replace("${SCRIPT}", command); + + // Parse the entrypoint as a shell command + // For now, we'll use a simple approach - split by spaces + // In the future, we might want to use proper shell parsing + let parts: Vec<&str> = expanded_entrypoint.split_whitespace().collect(); + if parts.is_empty() { + return Err(miette::miette!("Empty entrypoint in script metadata")); + } + + let cmd = parts[0].to_string(); + let mut args: Vec = parts[1..].iter().map(|s| s.to_string()).collect(); + + // If the entrypoint doesn't contain ${SCRIPT}, add the script path as the first argument + // This allows simple entrypoints like "python" to work as expected + if !entrypoint.contains("${SCRIPT}") { + args.push(command.to_string()); + } + + // Add any additional arguments provided via CLI + args.extend(command_iter.map(|s| s.clone())); + + (cmd, args) + } else { + // No entrypoint from metadata, use the command as-is + let command_args_vec: Vec = command_iter.map(|s| s.clone()).collect(); + (command.to_string(), command_args_vec) + }; + // Spawn the command - let mut cmd = std::process::Command::new(command); - let command_args_vec: Vec<_> = command_iter.collect(); - cmd.args(&command_args_vec); + let mut cmd = std::process::Command::new(&actual_command); + cmd.args(&actual_args); // On Windows, when using cmd.exe or cmd, we need to pass the full environment // because cmd.exe requires access to all environment variables (including prompt variables) @@ -148,7 +265,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { let status = cmd .status() .into_diagnostic() - .with_context(|| format!("failed to execute '{}'", &command))?; + .with_context(|| format!("failed to execute '{}'", &actual_command))?; // Return the exit code of the command std::process::exit(status.code().unwrap_or(1)); @@ -162,19 +279,31 @@ pub async fn create_exec_prefix( config: &Config, client: &ClientWithMiddleware, has_guessed_package: bool, + channels_from_metadata: Option<&[Channel]>, ) -> miette::Result { let command = args.command.first().expect("missing required command"); let specs = specs.to_vec(); - let channels = args - .channels - .resolve_from_config(config)? - .iter() - .map(|c| c.base_url.to_string()) - .collect(); + // Use channels from metadata if provided, otherwise use channels from args + let channels_for_hash = if let Some(metadata_channels) = channels_from_metadata { + metadata_channels + .iter() + .map(|c| c.base_url.to_string()) + .collect() + } else { + args.channels + .resolve_from_config(config)? + .iter() + .map(|c| c.base_url.to_string()) + .collect() + }; - let environment_hash = - EnvironmentHash::new(command.clone(), specs.clone(), channels, args.platform); + let environment_hash = EnvironmentHash::new( + command.clone(), + specs.clone(), + channels_for_hash, + args.platform, + ); let prefix = Prefix::new( cache_dir @@ -213,7 +342,15 @@ pub async fn create_exec_prefix( // Construct a gateway to get repodata. let gateway = config.gateway().with_client(client.clone()).finish(); - let channels = args.channels.resolve_from_config(config)?; + // Use channels from metadata if provided, otherwise resolve from args + let channels: Vec = if let Some(metadata_channels) = channels_from_metadata { + metadata_channels.to_vec() + } else { + args.channels + .resolve_from_config(config)? + .into_iter() + .collect() + }; // Get the repodata for the specs let repodata = await_in_progress("fetching repodata for environment", |_| async { diff --git a/crates/pixi_manifest/src/lib.rs b/crates/pixi_manifest/src/lib.rs index a3fb648080..3e14e2d402 100644 --- a/crates/pixi_manifest/src/lib.rs +++ b/crates/pixi_manifest/src/lib.rs @@ -17,6 +17,7 @@ mod preview; pub mod pypi; pub mod pyproject; mod s3; +pub mod script_metadata; mod solve_group; mod spec_type; mod system_requirements; diff --git a/crates/pixi_manifest/src/script_metadata.rs b/crates/pixi_manifest/src/script_metadata.rs new file mode 100644 index 0000000000..1dc58a3086 --- /dev/null +++ b/crates/pixi_manifest/src/script_metadata.rs @@ -0,0 +1,419 @@ +//! Parser for conda-script metadata embedded in script files. +//! +//! This module implements parsing of script metadata following the conda-script +//! specification. Metadata is embedded in comment blocks like: +//! +//! ```python +//! # /// conda-script +//! # [dependencies] +//! # python = "3.12.*" +//! # requests = "*" +//! # [script] +//! # channels = ["conda-forge"] +//! # entrypoint = "python" +//! # /// end-conda-script +//! ``` + +use indexmap::IndexMap; +use rattler_conda_types::{MatchSpec, NamedChannelOrUrl, Platform}; +use serde::{Deserialize, Serialize}; +use std::io::{BufRead, BufReader}; +use std::path::Path; +use std::str::FromStr; +use thiserror::Error; + +/// Errors that can occur when parsing script metadata +#[derive(Debug, Error, miette::Diagnostic)] +pub enum ScriptMetadataError { + #[error("Failed to read script file: {0}")] + IoError(#[from] std::io::Error), + + #[error("Failed to parse TOML metadata: {0}")] + TomlError(#[from] toml_edit::de::Error), + + #[error("No conda-script metadata block found in script")] + NoMetadataFound, + + #[error("Invalid matchspec '{0}': {1}")] + InvalidMatchSpec(String, String), + + #[error("Invalid channel URL '{0}': {1}")] + InvalidChannel(String, String), + + #[error("Malformed metadata block: {0}")] + MalformedBlock(String), +} + +/// Represents the complete conda-script metadata +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ScriptMetadata { + /// Package dependencies + #[serde(default)] + pub dependencies: DependenciesTable, + + /// Script configuration + pub script: ScriptTable, +} + +/// Dependencies table with platform-specific support +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct DependenciesTable { + /// Default dependencies (applies to all platforms) + #[serde(flatten)] + pub default: IndexMap, + + /// Platform-specific dependencies + #[serde(default, skip_serializing_if = "Option::is_none")] + pub target: Option>>, +} + +/// Script configuration table +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ScriptTable { + /// List of conda channels + pub channels: Vec, + + /// Command to run the script + #[serde(default)] + pub entrypoint: Option, + + /// Platform-specific script configuration + #[serde(default, skip_serializing_if = "Option::is_none")] + pub target: Option>, +} + +/// Platform-specific script configuration +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct PlatformScriptConfig { + /// Platform-specific entrypoint + #[serde(default)] + pub entrypoint: Option, +} + +impl ScriptMetadata { + /// Parse metadata from a script file + pub fn from_file(path: impl AsRef) -> Result { + let file = std::fs::File::open(path.as_ref())?; + let reader = BufReader::new(file); + Self::from_reader(reader) + } + + /// Parse metadata from a reader + pub fn from_reader(reader: R) -> Result { + let toml_content = extract_metadata_block(reader)?; + let metadata: ScriptMetadata = toml_edit::de::from_str(&toml_content)?; + Ok(metadata) + } + + /// Get all dependencies for the current platform + pub fn get_dependencies( + &self, + platform: Platform, + ) -> Result, ScriptMetadataError> { + let mut specs = Vec::new(); + + // Add default dependencies + for (name, version) in &self.dependencies.default { + let spec_str = if version == "*" || version.is_empty() { + name.clone() + } else { + format!("{}={}", name, version) + }; + let spec = + MatchSpec::from_str(&spec_str, rattler_conda_types::ParseStrictness::Lenient) + .map_err(|e| { + ScriptMetadataError::InvalidMatchSpec(spec_str.clone(), e.to_string()) + })?; + specs.push(spec); + } + + // Add platform-specific dependencies + if let Some(ref targets) = self.dependencies.target { + for (platform_selector, deps) in targets { + if Self::platform_matches(&platform_selector, platform) { + for (name, version) in deps { + let spec_str = if version == "*" || version.is_empty() { + name.clone() + } else { + format!("{}={}", name, version) + }; + let spec = MatchSpec::from_str( + &spec_str, + rattler_conda_types::ParseStrictness::Lenient, + ) + .map_err(|e| { + ScriptMetadataError::InvalidMatchSpec(spec_str.clone(), e.to_string()) + })?; + specs.push(spec); + } + } + } + } + + Ok(specs) + } + + /// Get channels + pub fn get_channels(&self) -> Result, ScriptMetadataError> { + self.script + .channels + .iter() + .map(|s| { + NamedChannelOrUrl::from_str(s) + .map_err(|e| ScriptMetadataError::InvalidChannel(s.clone(), e.to_string())) + }) + .collect() + } + + /// Get the entrypoint command for the current platform + pub fn get_entrypoint(&self, platform: Platform) -> Option { + // Check platform-specific entrypoint first + if let Some(ref targets) = self.script.target { + for (platform_selector, config) in targets { + if Self::platform_matches(platform_selector, platform) { + if let Some(ref entrypoint) = config.entrypoint { + return Some(entrypoint.clone()); + } + } + } + } + + // Fall back to default entrypoint + self.script.entrypoint.clone() + } + + /// Check if a platform selector matches the given platform + fn platform_matches(selector: &str, platform: Platform) -> bool { + match selector { + "unix" => matches!( + platform, + Platform::Linux64 + | Platform::LinuxAarch64 + | Platform::LinuxPpc64le + | Platform::Osx64 + | Platform::OsxArm64 + ), + "linux" => matches!( + platform, + Platform::Linux64 | Platform::LinuxAarch64 | Platform::LinuxPpc64le + ), + "osx" => matches!(platform, Platform::Osx64 | Platform::OsxArm64), + "win" => matches!(platform, Platform::Win64 | Platform::WinArm64), + specific => { + // Try to parse as a specific platform + if let Ok(specific_platform) = Platform::from_str(specific) { + specific_platform == platform + } else { + false + } + } + } + } +} + +/// Extract the conda-script metadata block from a reader +fn extract_metadata_block(reader: R) -> Result { + let mut in_block = false; + let mut toml_lines = Vec::new(); + let mut comment_prefix: Option = None; + + for line in reader.lines() { + let line = line?; + let trimmed = line.trim_start(); + + // Detect the start of the block + if !in_block && trimmed.contains("/// conda-script") { + in_block = true; + // Detect comment syntax (e.g., "#", "//", "--") + comment_prefix = detect_comment_prefix(&line); + continue; + } + + // Detect the end of the block + if in_block && trimmed.contains("/// end-conda-script") { + return Ok(toml_lines.join("\n")); + } + + // Extract TOML content inside the block + if in_block { + if let Some(ref prefix) = comment_prefix { + if let Some(content) = line.strip_prefix(prefix) { + // Remove the comment prefix and add to TOML content + toml_lines.push(content.trim_start().to_string()); + } else if trimmed.is_empty() { + // Allow empty lines + toml_lines.push(String::new()); + } else { + return Err(ScriptMetadataError::MalformedBlock(format!( + "Expected line to start with '{}', got: {}", + prefix, line + ))); + } + } + } + } + + if in_block { + return Err(ScriptMetadataError::MalformedBlock( + "Metadata block started but never closed with '/// end-conda-script'".to_string(), + )); + } + + Err(ScriptMetadataError::NoMetadataFound) +} + +/// Detect the comment prefix used in the script +fn detect_comment_prefix(line: &str) -> Option { + // Common comment prefixes + let prefixes = ["# ", "// ", "-- ", "/* "]; + + for prefix in &prefixes { + if line.trim_start().starts_with(prefix) { + return Some(prefix.to_string()); + } + } + + // Also support without trailing space + let prefixes_no_space = ["#", "//", "--"]; + for prefix in &prefixes_no_space { + if line.trim_start().starts_with(prefix) { + return Some(prefix.to_string()); + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + #[test] + fn test_parse_python_metadata() { + let script = r#"#!/usr/bin/env python +# Some header comment +# /// conda-script +# [dependencies] +# python = "3.12.*" +# requests = "*" +# [script] +# channels = ["conda-forge"] +# entrypoint = "python" +# /// end-conda-script + +import requests +print("Hello!") +"#; + + let cursor = Cursor::new(script); + let metadata = ScriptMetadata::from_reader(cursor).unwrap(); + + assert_eq!(metadata.dependencies.default.len(), 2); + assert_eq!( + metadata.dependencies.default.get("python").unwrap(), + "3.12.*" + ); + assert_eq!(metadata.dependencies.default.get("requests").unwrap(), "*"); + assert_eq!(metadata.script.channels, vec!["conda-forge"]); + assert_eq!(metadata.script.entrypoint, Some("python".to_string())); + } + + #[test] + fn test_parse_rust_metadata() { + let script = r#"// Rust script +// /// conda-script +// [dependencies] +// gcc = "*" +// [script] +// channels = ["conda-forge"] +// entrypoint = "cargo run" +// /// end-conda-script + +fn main() { + println!("Hello!"); +} +"#; + + let cursor = Cursor::new(script); + let metadata = ScriptMetadata::from_reader(cursor).unwrap(); + + assert_eq!(metadata.dependencies.default.len(), 1); + assert_eq!(metadata.dependencies.default.get("gcc").unwrap(), "*"); + } + + #[test] + fn test_parse_platform_specific_deps() { + let script = r#"# /// conda-script +# [dependencies] +# python = "3.12.*" +# [dependencies.target.unix] +# gcc = "*" +# [dependencies.target.win] +# msvc = "*" +# [script] +# channels = ["conda-forge"] +# /// end-conda-script +"#; + + let cursor = Cursor::new(script); + let metadata = ScriptMetadata::from_reader(cursor).unwrap(); + + // Test Unix platform + let deps_linux = metadata.get_dependencies(Platform::Linux64).unwrap(); + assert_eq!(deps_linux.len(), 2); // python + gcc + + // Test Windows platform + let deps_win = metadata.get_dependencies(Platform::Win64).unwrap(); + assert_eq!(deps_win.len(), 2); // python + msvc + } + + #[test] + fn test_platform_specific_entrypoint() { + let script = r#"# /// conda-script +# [dependencies] +# gcc = "*" +# [script] +# channels = ["conda-forge"] +# entrypoint = "bash default.sh" +# [script.target.win] +# entrypoint = "cmd.exe /c default.bat" +# /// end-conda-script +"#; + + let cursor = Cursor::new(script); + let metadata = ScriptMetadata::from_reader(cursor).unwrap(); + + let entrypoint_linux = metadata.get_entrypoint(Platform::Linux64); + assert_eq!(entrypoint_linux, Some("bash default.sh".to_string())); + + let entrypoint_win = metadata.get_entrypoint(Platform::Win64); + assert_eq!(entrypoint_win, Some("cmd.exe /c default.bat".to_string())); + } + + #[test] + fn test_no_metadata() { + let script = r#"#!/usr/bin/env python +print("No metadata here!") +"#; + + let cursor = Cursor::new(script); + let result = ScriptMetadata::from_reader(cursor); + assert!(matches!(result, Err(ScriptMetadataError::NoMetadataFound))); + } + + #[test] + fn test_unclosed_block() { + let script = r#"# /// conda-script +# [dependencies] +# python = "3.12.*" +"#; + + let cursor = Cursor::new(script); + let result = ScriptMetadata::from_reader(cursor); + assert!(matches!( + result, + Err(ScriptMetadataError::MalformedBlock(_)) + )); + } +} diff --git a/examples/conda-script/README.md b/examples/conda-script/README.md new file mode 100644 index 0000000000..a3f2acf0aa --- /dev/null +++ b/examples/conda-script/README.md @@ -0,0 +1,81 @@ +# Conda Script Examples + +This directory contains examples demonstrating the conda-script metadata feature in Pixi. + +## What is Conda Script? + +Conda script allows you to embed conda environment metadata directly in script files using specially formatted comment blocks. This makes scripts self-contained and easy to share. + +## Examples + +### 1. `hello_python.py` - Basic Example + +A simple Hello World script that shows the basics: +- Python version pinning +- Basic metadata structure +- How to run scripts with `pixi exec` + +```bash +pixi exec hello_python.py +``` + +### 2. `web_request.py` - External Dependencies + +Demonstrates using external packages (requests) from conda-forge: +- Adding package dependencies +- Using installed packages in your script + +```bash +pixi exec web_request.py +``` + +### 3. `platform_specific.py` - Platform-Specific Configuration + +Shows how to specify different dependencies for different platforms: +- Linux: installs `patchelf` +- macOS: installs `cctools` +- Windows: installs Visual Studio tools + +```bash +pixi exec platform_specific.py +``` + +## How to Use + +1. Make sure you have Pixi installed +2. Run any example with: `pixi exec ` +3. The first run will create an environment and install dependencies +4. Subsequent runs reuse the cached environment + +## Learn More + +See the [full documentation](../../docs/features/conda_script_metadata.md) for: +- Complete specification +- More examples +- Platform selectors +- Custom entrypoints +- Best practices + +## Creating Your Own + +To create your own conda-script: + +```python +#!/usr/bin/env python +# /// conda-script +# [dependencies] +# python = "3.12.*" +# your-package = "*" +# [script] +# channels = ["conda-forge"] +# entrypoint = "python" +# /// end-conda-script + +# Your code here +import your_package +``` + +Then run with: +```bash +pixi exec your_script.py +``` diff --git a/examples/conda-script/hello_python.py b/examples/conda-script/hello_python.py new file mode 100644 index 0000000000..a587497ac1 --- /dev/null +++ b/examples/conda-script/hello_python.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# /// conda-script +# [dependencies] +# python = "3.12.*" +# [script] +# channels = ["conda-forge"] +# entrypoint = "python" +# /// end-conda-script + +""" +A simple Hello World script demonstrating conda-script metadata. + +Run with: pixi exec hello_python.py +""" + +import sys +import platform + +print("=" * 60) +print("Hello from Python with conda-script!") +print("=" * 60) +print(f"Python version: {sys.version}") +print(f"Platform: {platform.system()} {platform.machine()}") +print(f"Executable: {sys.executable}") +print("=" * 60) diff --git a/examples/conda-script/platform_specific.py b/examples/conda-script/platform_specific.py new file mode 100644 index 0000000000..afc271ae83 --- /dev/null +++ b/examples/conda-script/platform_specific.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# /// conda-script +# [dependencies] +# python = "3.12.*" +# [dependencies.target.linux] +# patchelf = "*" +# [dependencies.target.osx] +# cctools = "*" +# [dependencies.target.win] +# vs2019_win-64 = "*" +# [script] +# channels = ["conda-forge"] +# entrypoint = "python" +# /// end-conda-script + +""" +Demonstrates platform-specific dependencies in conda-script. + +This script will install different tools depending on the platform: +- Linux: patchelf (for modifying ELF binaries) +- macOS: cctools (for Mach-O binary tools) +- Windows: Visual Studio 2019 tools + +Run with: pixi exec platform_specific.py +""" + +import platform +import shutil +import subprocess + + +def main(): + system = platform.system() + machine = platform.machine() + + print("=" * 60) + print("Platform-Specific Dependencies Demo") + print("=" * 60) + print(f"Platform: {system} ({machine})") + print() + + if system == "Linux": + print("On Linux - checking for patchelf...") + patchelf_path = shutil.which("patchelf") + if patchelf_path: + print(f"✓ Found patchelf at: {patchelf_path}") + result = subprocess.run(["patchelf", "--version"], capture_output=True, text=True) + print(f" Version: {result.stdout.strip()}") + else: + print("✗ patchelf not found") + + elif system == "Darwin": + print("On macOS - checking for otool (from cctools)...") + otool_path = shutil.which("otool") + if otool_path: + print(f"✓ Found otool at: {otool_path}") + result = subprocess.run(["otool", "-version"], capture_output=True, text=True) + print(f" Info: {result.stderr.strip()[:100]}") + else: + print("✗ otool not found") + + elif system == "Windows": + print("On Windows - Visual Studio tools should be available") + print("✓ VS2019 tools installed") + + else: + print(f"Unknown platform: {system}") + + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/examples/conda-script/web_request.py b/examples/conda-script/web_request.py new file mode 100644 index 0000000000..cf3b1289d1 --- /dev/null +++ b/examples/conda-script/web_request.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# /// conda-script +# [dependencies] +# python = "3.12.*" +# requests = "*" +# [script] +# channels = ["conda-forge"] +# entrypoint = "python" +# /// end-conda-script + +""" +Fetch and display information from the GitHub API. + +Demonstrates using external dependencies (requests) with conda-script. + +Run with: pixi exec web_request.py +""" + +import requests + + +def main(): + print("Fetching GitHub API information...") + + # Fetch GitHub API root + response = requests.get("https://api.github.com") + + print(f"\nStatus Code: {response.status_code}") + print(f"Requests Version: {requests.__version__}") + + # Pretty print some of the response + data = response.json() + print("\nAvailable endpoints:") + for key, value in list(data.items())[:5]: + print(f" {key}: {value}") + + +if __name__ == "__main__": + main()