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
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ urlencoding = "2.1"
walkdir = "2.5.0"
which = "8.0.2"
windows-sys = "0.61"
winreg = "0.56"
zip = { version = "8.5.1", default-features = false, features = ["deflate"] }
zstd = "0.13.3"
spdx = "0.13.4"
Expand Down
3 changes: 3 additions & 0 deletions crates/rattler_build_source_cache/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ sigstore-verify = { version = "0.6.4", default-features = false, optional = true
sigstore-trust-root = { version = "0.6.4", default-features = false, optional = true }
sigstore-types = { version = "0.6.4", optional = true }

[target.'cfg(target_os = "windows")'.dependencies]
winreg = { workspace = true }

[dev-dependencies]
tokio = { workspace = true, features = ["rt", "macros", "test-util"] }
tempfile.workspace = true
54 changes: 44 additions & 10 deletions crates/rattler_build_source_cache/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,41 @@ pub fn is_tarball(file_name: &str) -> bool {
.any(|ext| file_name.ends_with(ext))
}

fn extract_tar(archive: &Path, target: &Path) -> Result<(), CacheError> {
/// On Windows, checks whether Developer Mode is enabled by reading the registry key
/// `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock` value
/// `AllowDevelopmentWithoutDevLicense`.
/// Returns `false` if the key is missing, inaccessible, or the value is not `1`.
#[cfg(target_os = "windows")]
fn is_windows_developer_mode_enabled() -> bool {
use winreg::RegKey;
use winreg::enums::HKEY_LOCAL_MACHINE;

(|| -> Result<bool, std::io::Error> {
let value: u32 = RegKey::predef(HKEY_LOCAL_MACHINE)
.open_subkey(r"SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock")?
.get_value("AllowDevelopmentWithoutDevLicense")?;
Ok(value == 1)
})()
.unwrap_or(false)
}

/// Wraps a tar unpack error, appending a Windows Developer Mode hint when on Windows and
/// Developer Mode is detected as disabled.
fn tar_unpack_error(context: &str, e: std::io::Error) -> CacheError {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I assume this is also a specific io error that we are looking for? Permission denied or something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not reliably. The error code when symlink creation fails on Windows with Developer Mode disabled is not consistent. ERROR_PRIVILEGE_NOT_HELD (1314) is the most common (as per my observation), but it can't reliably help in identifying a "symlink failed" error from the error alone, we instead gate the hint on the Developer Mode registry check: if Developer Mode is off and tar extraction fails for any reason, we surface the hint. The hint copy uses "may contain symbolic links" to reflect this uncertainty.

#[cfg(target_os = "windows")]
if !is_windows_developer_mode_enabled() {
return CacheError::ExtractionError(format!(
"{context}: {e}\n\n\
hint: Extracting this archive failed on Windows. The archive may contain symbolic \
links, which require Developer Mode or administrator privileges to create. \
Please enable Developer Mode in: \
Developer settings > Developer Mode, then retry the build."
));
}
CacheError::ExtractionError(format!("{context}: {e}"))
}

pub(crate) fn extract_tar(archive: &Path, target: &Path) -> Result<(), CacheError> {
let file = fs_err::File::open(archive)
.map_err(|e| CacheError::ExtractionError(format!("Failed to open archive: {}", e)))?;

Expand All @@ -721,30 +755,30 @@ fn extract_tar(archive: &Path, target: &Path) -> Result<(), CacheError> {
let mut archive = tar::Archive::new(GzDecoder::new(file));
archive
.unpack(target)
.map_err(|e| CacheError::ExtractionError(format!("Failed to extract tar.gz: {}", e)))?;
.map_err(|e| tar_unpack_error("Failed to extract tar.gz", e))?;
} else if name.ends_with(".tar.bz2") || name.ends_with(".tbz2") {
let mut archive = tar::Archive::new(bzip2::read::BzDecoder::new(file));
archive.unpack(target).map_err(|e| {
CacheError::ExtractionError(format!("Failed to extract tar.bz2: {}", e))
})?;
archive
.unpack(target)
.map_err(|e| tar_unpack_error("Failed to extract tar.bz2", e))?;
} else if name.ends_with(".tar.xz") || name.ends_with(".txz") {
let mut archive = tar::Archive::new(lzma_rust2::XzReader::new(file, true));
archive
.unpack(target)
.map_err(|e| CacheError::ExtractionError(format!("Failed to extract tar.xz: {}", e)))?;
.map_err(|e| tar_unpack_error("Failed to extract tar.xz", e))?;
} else if name.ends_with(".tar.zst") {
let decoder = zstd::stream::read::Decoder::new(file).map_err(|e| {
CacheError::ExtractionError(format!("Failed to create zstd decoder: {}", e))
})?;
let mut archive = tar::Archive::new(decoder);
archive.unpack(target).map_err(|e| {
CacheError::ExtractionError(format!("Failed to extract tar.zst: {}", e))
})?;
archive
.unpack(target)
.map_err(|e| tar_unpack_error("Failed to extract tar.zst", e))?;
} else {
let mut archive = tar::Archive::new(file);
archive
.unpack(target)
.map_err(|e| CacheError::ExtractionError(format!("Failed to extract tar: {}", e)))?;
.map_err(|e| tar_unpack_error("Failed to extract tar", e))?;
}

Ok(())
Expand Down
49 changes: 49 additions & 0 deletions crates/rattler_build_source_cache/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,55 @@ mod source_cache_tests {
);
}

/// Creates a minimal `.tar.gz` at `dest` containing a single symlink entry.
#[cfg(target_os = "windows")]
fn make_tar_gz_with_symlink(dest: &std::path::Path) {
use flate2::{Compression, write::GzEncoder};

let file = fs_err::File::create(dest).unwrap();
let enc = GzEncoder::new(file, Compression::default());
let mut builder = tar::Builder::new(enc);

let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Symlink);
header.set_size(0);
header.set_mode(0o777);
header.set_cksum();
builder
.append_link(&mut header, "link_target", "real_file")
.unwrap();
builder.finish().unwrap();
}

/// On Windows without Developer Mode the extraction of a symlink-containing
/// archive should fail with a hint about enabling Developer Mode.
/// On Windows with Developer Mode enabled the extraction succeeds.
#[test]
#[cfg(target_os = "windows")]
fn test_symlink_extraction_error_message() {
use crate::cache::extract_tar;

let dir = TempDir::new().unwrap();
let archive = dir.path().join("test.tar.gz");
make_tar_gz_with_symlink(&archive);

let target = dir.path().join("out");
fs_err::create_dir_all(&target).unwrap();

match extract_tar(&archive, &target) {
Ok(()) => {
// Symlinks extracted fine — Developer Mode is on (or not Windows)
}
Err(CacheError::ExtractionError(msg)) => {
assert!(
msg.contains("Developer Mode"),
"Expected Developer Mode hint in error message, got: {msg}"
);
}
Err(e) => panic!("Unexpected error variant: {e}"),
}
}

#[test]
fn test_extract_filename_from_header_strips_path_components() {
use crate::cache::extract_filename_from_header;
Expand Down
11 changes: 11 additions & 0 deletions py-rattler-build/rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.