Skip to content

Commit 5845966

Browse files
committed
Add Windows symlink detection and binary prefix detection tests
1 parent 712228e commit 5845966

File tree

7 files changed

+230
-2
lines changed

7 files changed

+230
-2
lines changed

src/build.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use std::{path::PathBuf, vec};
44

55
use miette::{Context, IntoDiagnostic};
6-
use rattler_conda_types::{Channel, MatchSpec};
6+
use rattler_conda_types::{Channel, MatchSpec, package::PathsJson};
77

88
use crate::{
99
metadata::{Output, build_reindexed_channels},
@@ -138,6 +138,20 @@ pub async fn run_build(
138138
.await
139139
.into_diagnostic()?;
140140

141+
// Check for binary prefix if configured
142+
if tool_configuration.error_prefix_in_binary {
143+
tracing::info!("Checking for host prefix in binary files...");
144+
check_for_binary_prefix(&output, &paths_json)?;
145+
}
146+
147+
// Check for symlinks on Windows if not allowed
148+
if output.build_configuration.target_platform.is_windows()
149+
&& !tool_configuration.allow_symlinks_on_windows
150+
{
151+
tracing::info!("Checking for symlinks in Windows package...");
152+
check_for_symlinks_on_windows(&output, &paths_json)?;
153+
}
154+
141155
output.record_artifact(&result, &paths_json);
142156

143157
let span = tracing::info_span!("Running package tests");
@@ -164,3 +178,51 @@ pub async fn run_build(
164178

165179
Ok((output, result))
166180
}
181+
182+
/// Check if any binary files contain the host prefix
183+
fn check_for_binary_prefix(output: &Output, paths_json: &PathsJson) -> Result<(), miette::Error> {
184+
use rattler_conda_types::package::FileMode;
185+
186+
for paths_entry in &paths_json.paths {
187+
if let Some(prefix_placeholder) = &paths_entry.prefix_placeholder {
188+
if prefix_placeholder.file_mode == FileMode::Binary {
189+
return Err(miette::miette!(
190+
"Package {} contains Binary file {} which contains host prefix placeholder, which may cause issues when the package is installed to a different location. \
191+
Consider fixing the build process to avoid embedding the host prefix in binaries. \
192+
To allow this, remove the --error-prefix-in-binary flag.",
193+
output.name().as_normalized(),
194+
paths_entry.relative_path.display()
195+
));
196+
}
197+
}
198+
}
199+
200+
Ok(())
201+
}
202+
203+
/// Check if any files are symlinks on Windows
204+
fn check_for_symlinks_on_windows(
205+
output: &Output,
206+
paths_json: &PathsJson,
207+
) -> Result<(), miette::Error> {
208+
use rattler_conda_types::package::PathType;
209+
210+
let mut symlinks = Vec::new();
211+
212+
for paths_entry in &paths_json.paths {
213+
if paths_entry.path_type == PathType::SoftLink {
214+
symlinks.push(paths_entry.relative_path.display().to_string());
215+
}
216+
}
217+
218+
if !symlinks.is_empty() {
219+
return Err(miette::miette!(
220+
"Package {} contains symlinks which are not supported on most Windows systems:\n - {}\n\
221+
To allow symlinks, use the --allow-symlinks-on-windows flag.",
222+
output.name().as_normalized(),
223+
symlinks.join("\n - ")
224+
));
225+
}
226+
227+
Ok(())
228+
}

src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ pub fn get_tool_config(
148148
.with_continue_on_failure(build_data.continue_on_failure)
149149
.with_noarch_build_platform(build_data.noarch_build_platform)
150150
.with_channel_priority(build_data.common.channel_priority)
151-
.with_allow_insecure_host(build_data.common.allow_insecure_host.clone());
151+
.with_allow_insecure_host(build_data.common.allow_insecure_host.clone())
152+
.with_error_prefix_in_binary(build_data.error_prefix_in_binary)
153+
.with_allow_symlinks_on_windows(build_data.allow_symlinks_on_windows);
152154

153155
let configuration_builder = if let Some(fancy_log_handler) = fancy_log_handler {
154156
configuration_builder.with_logging_output_handler(fancy_log_handler.clone())
@@ -996,6 +998,8 @@ pub async fn debug_recipe(
996998
extra_meta: None,
997999
sandbox_configuration: None,
9981000
continue_on_failure: ContinueOnFailure::No,
1001+
error_prefix_in_binary: false,
1002+
allow_symlinks_on_windows: false,
9991003
};
10001004

10011005
let tool_config = get_tool_config(&build_data, log_handler)?;

src/opt.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,14 @@ pub struct BuildOpts {
446446
/// This is useful when building many packages with `--recipe-dir`.`
447447
#[clap(long)]
448448
pub continue_on_failure: bool,
449+
450+
/// Error if the host prefix is detected in any binary files
451+
#[arg(long, help_heading = "Modifying result")]
452+
pub error_prefix_in_binary: bool,
453+
454+
/// Allow symlinks in packages on Windows (defaults to false - symlinks are forbidden on Windows)
455+
#[arg(long, help_heading = "Modifying result")]
456+
pub allow_symlinks_on_windows: bool,
449457
}
450458
#[allow(missing_docs)]
451459
#[derive(Clone, Debug)]
@@ -475,6 +483,8 @@ pub struct BuildData {
475483
pub sandbox_configuration: Option<SandboxConfiguration>,
476484
pub debug: Debug,
477485
pub continue_on_failure: ContinueOnFailure,
486+
pub error_prefix_in_binary: bool,
487+
pub allow_symlinks_on_windows: bool,
478488
}
479489

480490
impl BuildData {
@@ -505,6 +515,8 @@ impl BuildData {
505515
sandbox_configuration: Option<SandboxConfiguration>,
506516
debug: bool,
507517
continue_on_failure: ContinueOnFailure,
518+
error_prefix_in_binary: bool,
519+
allow_symlinks_on_windows: bool,
508520
) -> Self {
509521
Self {
510522
up_to,
@@ -539,6 +551,8 @@ impl BuildData {
539551
sandbox_configuration,
540552
debug: Debug::new(debug),
541553
continue_on_failure,
554+
error_prefix_in_binary,
555+
allow_symlinks_on_windows,
542556
}
543557
}
544558
}
@@ -584,6 +598,8 @@ impl BuildData {
584598
opts.sandbox_arguments.into(),
585599
opts.debug,
586600
opts.continue_on_failure.into(),
601+
opts.error_prefix_in_binary,
602+
opts.allow_symlinks_on_windows,
587603
)
588604
}
589605
}

src/tool_configuration.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,12 @@ pub struct Configuration {
218218

219219
/// Whether to continue building on failure of a package or stop the build
220220
pub continue_on_failure: ContinueOnFailure,
221+
222+
/// Whether to error if the host prefix is detected in binary files
223+
pub error_prefix_in_binary: bool,
224+
225+
/// Whether to allow symlinks in packages on Windows (defaults to false)
226+
pub allow_symlinks_on_windows: bool,
221227
}
222228

223229
/// Get the authentication storage from the given file
@@ -272,6 +278,8 @@ pub struct ConfigurationBuilder {
272278
channel_priority: ChannelPriority,
273279
allow_insecure_host: Option<Vec<String>>,
274280
continue_on_failure: ContinueOnFailure,
281+
error_prefix_in_binary: bool,
282+
allow_symlinks_on_windows: bool,
275283
}
276284

277285
impl Configuration {
@@ -301,6 +309,8 @@ impl ConfigurationBuilder {
301309
channel_priority: ChannelPriority::Strict,
302310
allow_insecure_host: None,
303311
continue_on_failure: ContinueOnFailure::No,
312+
error_prefix_in_binary: false,
313+
allow_symlinks_on_windows: false,
304314
}
305315
}
306316

@@ -321,6 +331,22 @@ impl ConfigurationBuilder {
321331
}
322332
}
323333

334+
/// Whether to error if the host prefix is detected in binary files
335+
pub fn with_error_prefix_in_binary(self, error_prefix_in_binary: bool) -> Self {
336+
Self {
337+
error_prefix_in_binary,
338+
..self
339+
}
340+
}
341+
342+
/// Whether to allow symlinks in packages on Windows
343+
pub fn with_allow_symlinks_on_windows(self, allow_symlinks_on_windows: bool) -> Self {
344+
Self {
345+
allow_symlinks_on_windows,
346+
..self
347+
}
348+
}
349+
324350
/// Set the default cache directory to use for objects that need to be
325351
/// cached.
326352
pub fn with_opt_cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
@@ -492,6 +518,8 @@ impl ConfigurationBuilder {
492518
channel_priority: self.channel_priority,
493519
allow_insecure_host: self.allow_insecure_host,
494520
continue_on_failure: self.continue_on_failure,
521+
error_prefix_in_binary: self.error_prefix_in_binary,
522+
allow_symlinks_on_windows: self.allow_symlinks_on_windows,
495523
}
496524
}
497525
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
context:
2+
name: binary-prefix-test
3+
version: "1.0.0"
4+
5+
package:
6+
name: ${{ name }}
7+
version: ${{ version }}
8+
9+
build:
10+
number: 0
11+
script:
12+
- if: unix
13+
then:
14+
- mkdir -p $PREFIX/bin
15+
# C program that embeds the prefix
16+
- |
17+
cat > test_binary.c << EOF
18+
#include <stdio.h>
19+
int main() {
20+
const char* prefix = "$PREFIX";
21+
printf("Prefix is: %s\n", prefix);
22+
return 0;
23+
}
24+
EOF
25+
- gcc test_binary.c -o $PREFIX/bin/test_binary
26+
else:
27+
- mkdir %PREFIX%\Library\bin
28+
# A simple executable that contains the prefix
29+
- |
30+
echo #include ^<stdio.h^> > test_binary.c
31+
echo int main() { >> test_binary.c
32+
echo const char* prefix = "%PREFIX%"; >> test_binary.c
33+
echo printf("Prefix is: %%s\n", prefix); >> test_binary.c
34+
echo return 0; >> test_binary.c
35+
echo } >> test_binary.c
36+
- cl test_binary.c /Fe:%PREFIX%\Library\bin\test_binary.exe
37+
38+
requirements:
39+
build:
40+
- ${{ compiler('c') }}
41+
42+
about:
43+
summary: Test package with binary containing host prefix
44+
description: This package intentionally contains a binary with the host prefix embedded
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
context:
2+
name: symlink-test
3+
version: "1.0.0"
4+
5+
package:
6+
name: ${{ name }}
7+
version: ${{ version }}
8+
9+
build:
10+
number: 0
11+
script:
12+
- mkdir %PREFIX%\Library\bin
13+
- echo @echo off > %PREFIX%\Library\bin\real_script.bat
14+
- echo echo Hello from real script >> %PREFIX%\Library\bin\real_script.bat
15+
- mklink %PREFIX%\Library\bin\symlink_script.bat %PREFIX%\Library\bin\real_script.bat
16+
- mklink %PREFIX%\Library\bin\another_symlink.bat %PREFIX%\Library\bin\real_script.bat
17+
18+
about:
19+
summary: Test package with symlinks
20+
description: This package contains symlinks to test Windows symlink detection

test/end-to-end/test_simple.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import requests
1414
import yaml
1515
import subprocess
16+
import shutil
1617
from helpers import RattlerBuild, check_build_output, get_extracted_package, get_package
1718

1819

@@ -1870,3 +1871,56 @@ def test_merge_build_and_host(
18701871
recipes / "merge_build_and_host/recipe.yaml",
18711872
tmp_path,
18721873
)
1874+
1875+
1876+
def test_error_on_binary_prefix(
1877+
rattler_build: RattlerBuild, recipes: Path, tmp_path: Path
1878+
):
1879+
"""Test that --error-prefix-in-binary flag correctly detects prefix in binaries"""
1880+
recipe_path = recipes / "binary_prefix_test"
1881+
args = rattler_build.build_args(recipe_path, tmp_path)
1882+
rattler_build(*args)
1883+
1884+
shutil.rmtree(tmp_path)
1885+
tmp_path.mkdir()
1886+
args = rattler_build.build_args(recipe_path, tmp_path)
1887+
args = list(args) + ["--error-prefix-in-binary"]
1888+
1889+
try:
1890+
rattler_build(*args, stderr=STDOUT)
1891+
pytest.fail("Expected build to fail with binary prefix error")
1892+
except CalledProcessError as e:
1893+
output = e.output.decode("utf-8") if e.output else ""
1894+
assert "Binary file" in output and "contains host prefix" in output
1895+
1896+
1897+
@pytest.mark.skipif(
1898+
os.name != "nt", reason="Windows symlink test can only run on Windows"
1899+
)
1900+
def test_error_on_symlinks_windows(
1901+
rattler_build: RattlerBuild, recipes: Path, tmp_path: Path
1902+
):
1903+
"""Test that symlinks are forbidden on Windows by default"""
1904+
recipe_path = recipes / "symlink_test"
1905+
1906+
args = rattler_build.build_args(recipe_path, tmp_path)
1907+
1908+
# Should fail by default on Windows
1909+
try:
1910+
rattler_build(*args, stderr=STDOUT)
1911+
pytest.fail("Expected build to fail with symlink error on Windows")
1912+
except CalledProcessError as e:
1913+
output = e.output.decode("utf-8") if e.output else ""
1914+
assert "symlinks" in output.lower() and "windows systems" in output.lower()
1915+
1916+
# Should succeed with --allow-symlinks-on-windows flag
1917+
shutil.rmtree(tmp_path)
1918+
tmp_path.mkdir()
1919+
args = rattler_build.build_args(recipe_path, tmp_path)
1920+
args = list(args) + ["--allow-symlinks-on-windows"]
1921+
rattler_build(*args)
1922+
pkg_name = "symlink-test-1.0.0-h"
1923+
assert any(
1924+
f.name.startswith(pkg_name)
1925+
for f in (tmp_path / host_subdir()).glob("*.tar.bz2")
1926+
)

0 commit comments

Comments
 (0)