Skip to content

feat: implement binary prefix detection behavior in packaging #1658

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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 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
2 changes: 2 additions & 0 deletions py-rattler-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ fn build_recipes_py(
None,
Debug::new(debug),
continue_on_failure.into(),
false,
false,
);

let rt = tokio::runtime::Runtime::new().unwrap();
Expand Down
64 changes: 63 additions & 1 deletion src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use std::{path::PathBuf, vec};

use miette::{Context, IntoDiagnostic};
use rattler_conda_types::{Channel, MatchSpec};
use rattler_conda_types::{Channel, MatchSpec, package::PathsJson};

use crate::{
metadata::{Output, build_reindexed_channels},
Expand Down Expand Up @@ -150,6 +150,20 @@ pub async fn run_build(
.await
.into_diagnostic()?;

// Check for binary prefix if configured
if tool_configuration.error_prefix_in_binary {
tracing::info!("Checking for host prefix in binary files...");
check_for_binary_prefix(&output, &paths_json)?;
}

// Check for symlinks on Windows if not allowed
if output.build_configuration.target_platform.is_windows()
&& !tool_configuration.allow_symlinks_on_windows
{
tracing::info!("Checking for symlinks in Windows package...");
check_for_symlinks_on_windows(&output, &paths_json)?;
}

output.record_artifact(&result, &paths_json);

let span = tracing::info_span!("Running package tests");
Expand All @@ -176,3 +190,51 @@ pub async fn run_build(

Ok((output, result))
}

/// Check if any binary files contain the host prefix
fn check_for_binary_prefix(output: &Output, paths_json: &PathsJson) -> Result<(), miette::Error> {
use rattler_conda_types::package::FileMode;

for paths_entry in &paths_json.paths {
if let Some(prefix_placeholder) = &paths_entry.prefix_placeholder {
if prefix_placeholder.file_mode == FileMode::Binary {
return Err(miette::miette!(
"Package {} contains Binary file {} which contains host prefix placeholder, which may cause issues when the package is installed to a different location. \
Consider fixing the build process to avoid embedding the host prefix in binaries. \
To allow this, remove the --error-prefix-in-binary flag.",
output.name().as_normalized(),
paths_entry.relative_path.display()
));
}
}
}

Ok(())
}

/// Check if any files are symlinks on Windows
fn check_for_symlinks_on_windows(
output: &Output,
paths_json: &PathsJson,
) -> Result<(), miette::Error> {
use rattler_conda_types::package::PathType;

let mut symlinks = Vec::new();

for paths_entry in &paths_json.paths {
if paths_entry.path_type == PathType::SoftLink {
symlinks.push(paths_entry.relative_path.display().to_string());
}
}

if !symlinks.is_empty() {
return Err(miette::miette!(
"Package {} contains symlinks which are not supported on most Windows systems:\n - {}\n\
To allow symlinks, use the --allow-symlinks-on-windows flag.",
output.name().as_normalized(),
symlinks.join("\n - ")
));
}

Ok(())
}
6 changes: 5 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ pub fn get_tool_config(
.with_continue_on_failure(build_data.continue_on_failure)
.with_noarch_build_platform(build_data.noarch_build_platform)
.with_channel_priority(build_data.common.channel_priority)
.with_allow_insecure_host(build_data.common.allow_insecure_host.clone());
.with_allow_insecure_host(build_data.common.allow_insecure_host.clone())
.with_error_prefix_in_binary(build_data.error_prefix_in_binary)
.with_allow_symlinks_on_windows(build_data.allow_symlinks_on_windows);

let configuration_builder = if let Some(fancy_log_handler) = fancy_log_handler {
configuration_builder.with_logging_output_handler(fancy_log_handler.clone())
Expand Down Expand Up @@ -1025,6 +1027,8 @@ pub async fn debug_recipe(
extra_meta: None,
sandbox_configuration: None,
continue_on_failure: ContinueOnFailure::No,
error_prefix_in_binary: false,
allow_symlinks_on_windows: false,
};

let tool_config = get_tool_config(&build_data, log_handler)?;
Expand Down
16 changes: 16 additions & 0 deletions src/opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,14 @@ pub struct BuildOpts {
/// This is useful when building many packages with `--recipe-dir`.`
#[clap(long)]
pub continue_on_failure: bool,

/// Error if the host prefix is detected in any binary files
#[arg(long, help_heading = "Modifying result")]
pub error_prefix_in_binary: bool,

/// Allow symlinks in packages on Windows (defaults to false - symlinks are forbidden on Windows)
#[arg(long, help_heading = "Modifying result")]
pub allow_symlinks_on_windows: bool,
}
#[allow(missing_docs)]
#[derive(Clone, Debug)]
Expand Down Expand Up @@ -475,6 +483,8 @@ pub struct BuildData {
pub sandbox_configuration: Option<SandboxConfiguration>,
pub debug: Debug,
pub continue_on_failure: ContinueOnFailure,
pub error_prefix_in_binary: bool,
pub allow_symlinks_on_windows: bool,
}

impl BuildData {
Expand Down Expand Up @@ -505,6 +515,8 @@ impl BuildData {
sandbox_configuration: Option<SandboxConfiguration>,
debug: Debug,
continue_on_failure: ContinueOnFailure,
error_prefix_in_binary: bool,
allow_symlinks_on_windows: bool,
) -> Self {
Self {
up_to,
Expand Down Expand Up @@ -539,6 +551,8 @@ impl BuildData {
sandbox_configuration,
debug,
continue_on_failure,
error_prefix_in_binary,
allow_symlinks_on_windows,
}
}
}
Expand Down Expand Up @@ -584,6 +598,8 @@ impl BuildData {
opts.sandbox_arguments.into(),
Debug::new(opts.debug),
opts.continue_on_failure.into(),
opts.error_prefix_in_binary,
opts.allow_symlinks_on_windows,
)
}
}
Expand Down
98 changes: 45 additions & 53 deletions src/packaging/file_mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use crate::metadata::Output;
use fs_err as fs;
#[cfg(target_family = "unix")]
use fs_err::os::unix::fs::symlink;

Check failure on line 6 in src/packaging/file_mapper.rs

View workflow job for this annotation

GitHub Actions / Build x86_64-unknown-linux-musl

unused import: `fs_err::os::unix::fs::symlink`

Check failure on line 6 in src/packaging/file_mapper.rs

View workflow job for this annotation

GitHub Actions / Build aarch64-apple-darwin

unused import: `fs_err::os::unix::fs::symlink`
use std::{
collections::HashSet,
path::{Component, Path, PathBuf},
Expand Down Expand Up @@ -220,63 +220,55 @@

let metadata = fs::symlink_metadata(path)?;

// make absolute symlinks relative
if metadata.is_symlink() {
// Handle symlinks: make absolute symlinks relative and copy the link
if metadata.file_type().is_symlink() {
if target_platform.is_windows() {
tracing::warn!("Symlinks need administrator privileges on Windows");
tracing::warn!("Symlink creation on Windows requires administrator privileges");
}

if let Result::Ok(link) = fs::read_link(path) {
tracing::trace!("Copying link: {:?} -> {:?}", path, link);
} else {
tracing::warn!("Could not read link at {:?}", path);
}

#[cfg(target_family = "unix")]
fs::read_link(path).and_then(|target| {
if target.is_absolute() && target.starts_with(prefix) {
let rel_target = pathdiff::diff_paths(
target,
path.parent().ok_or(std::io::Error::new(
std::io::ErrorKind::Other,
"Could not get parent directory",
))?,
)
.ok_or(std::io::Error::new(
std::io::ErrorKind::Other,
"Could not get relative path",
))?;

tracing::trace!(
"Making symlink relative {:?} -> {:?}",
dest_path,
rel_target
);
symlink(&rel_target, &dest_path).map_err(|e| {
tracing::error!(
"Could not create symlink from {:?} to {:?}: {:?}",
rel_target,
dest_path,
e
);
e
})?;
} else {
if target.is_absolute() {
tracing::warn!("Symlink {:?} points outside of the prefix", path);
// Read the link target
match fs::read_link(path) {
Ok(mut target) => {
// If absolute and within the build prefix, make it relative
if target.is_absolute() && target.starts_with(prefix) {
if let Some(parent) = path.parent() {
if let Some(rel) = pathdiff::diff_paths(&target, parent) {
target = rel;
}
}
}
// Create the symlink at dest_path
#[cfg(unix)]
{
if let Err(e) = fs::os::unix::fs::symlink(&target, &dest_path) {
tracing::warn!(
"Failed to create symlink {:?} -> {:?}: {:?}",
dest_path,
target,
e
);
}
}
#[cfg(windows)]
{
let res = if metadata.file_type().is_dir() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the metadata is the symlink_metadata - is that correct, and will it ever evaluate is_dir() == true?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just checked and i think we can use symlink_dir as well alongside with symlink_metadata to check if first if the target is a directory or not, will add this too now

std::os::windows::fs::symlink_dir(&target, &dest_path)
} else {
std::os::windows::fs::symlink_file(&target, &dest_path)
};
if let Err(e) = res {
tracing::warn!(
"Failed to create symlink {:?} -> {:?}: {:?}",
dest_path,
target,
e
);
}
}
symlink(&target, &dest_path).map_err(|e| {
tracing::error!(
"Could not create symlink from {:?} to {:?}: {:?}",
target,
dest_path,
e
);
e
})?;
}
Result::Ok(())
})?;
Err(e) => {
tracing::warn!("Failed to read symlink {:?}: {:?}", path, e);
}
}
Ok(Some(dest_path))
} else if metadata.is_dir() {
// skip directories for now
Expand Down
41 changes: 10 additions & 31 deletions src/packaging/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
//! Functions to write and create metadata from a given output

#[cfg(target_family = "unix")]
use std::os::unix::prelude::OsStrExt;
use std::{
borrow::Cow,
collections::HashSet,
Expand All @@ -27,33 +25,22 @@ use super::{PackagingError, TempFiles};
use crate::{hash::HashInput, metadata::Output, recipe::parser::PrefixDetection};

/// Detect if the file contains the prefix in binary mode.
#[allow(unused_variables)]
pub fn contains_prefix_binary(file_path: &Path, prefix: &Path) -> Result<bool, PackagingError> {
// Convert the prefix to a Vec<u8> for binary comparison
// TODO on Windows check both ascii and utf-8 / 16?
#[cfg(target_family = "windows")]
{
tracing::warn!("Windows is not supported yet for binary prefix checking.");
Ok(false)
}

#[cfg(target_family = "unix")]
{
let prefix_bytes = prefix.as_os_str().as_bytes().to_vec();
let prefix_bytes = prefix.to_string_lossy().as_bytes().to_vec();

// Open the file
let file = File::open(file_path)?;
// Open the file
let file = File::open(file_path)?;

// Read the file's content
let data = unsafe { memmap2::Mmap::map(&file) }?;
// Read the file's content
let data = unsafe { memmap2::Mmap::map(&file) }?;

// Check if the content contains the prefix bytes with memchr
let contains_prefix = memchr::memmem::find_iter(data.as_ref(), &prefix_bytes)
.next()
.is_some();
// Check if the content contains the prefix bytes with memchr
let contains_prefix = memchr::memmem::find_iter(data.as_ref(), &prefix_bytes)
.next()
.is_some();

Ok(contains_prefix)
}
Ok(contains_prefix)
}

/// This function requires we know the file content we are matching against is
Expand Down Expand Up @@ -171,14 +158,6 @@ pub fn create_prefix_placeholder(
return Ok(None);
}

if target_platform.is_windows() {
tracing::debug!(
"Binary prefix replacement is not performed fors Windows: {:?}",
relative_path
);
return Ok(None);
}

if contains_prefix_binary(file_path, encoded_prefix)? {
has_prefix = Some(encoded_prefix.to_string_lossy().to_string());
}
Expand Down
Loading
Loading