Repository: prefix-dev/rattler-build (rust)

Issue:
Auto-discover interpreter based on script filename
We can set the interpreter manually like:

```
script:
  interpreter: python
  file: foobar.py
```

However, it would be nice if we can auto-discover the interpreter based on the filename, so that we can write:

```
script: foobar.py
```

And it will automatically choose the `python` interpreter. Same for Perl, R, Bash, cmd.exe, ...

Pull request description:
feat: add interpreter detection for various script types
closes #1611

i was debating whetever i should add a UTF-8 validation but i thought it should be added just in case for windows computers with asian languages, let me know what you think about it @wolfv 

Also, as shown in recipe examples we still hold the requirements/run to not to cause a magicial workflow, because i thought people want to control exactly what they want to run with

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 4ffc01d5..a37f0aa4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4336,6 +4336,7 @@ dependencies = [
  "tracing-subscriber",
  "tracing-test",
  "tui-input",
+ "unicode-normalization",
  "url",
  "walkdir",
  "which",
diff --git a/Cargo.toml b/Cargo.toml
index e875f975..6785f587 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -176,6 +176,7 @@ lazy_static = "1.5.0"
 reqwest-retry = "0.7.0"
 retry-policies = "0.4.0"
 strum = "0.27.1"
+unicode-normalization = "0.1.24"
 
 [target.'cfg(not(target_os = "windows"))'.dependencies]
 sha2 = { version = "0.10.8", features = ["asm"] }
diff --git a/pixi.lock b/pixi.lock
index fd08631f..afff230d 100644
--- a/pixi.lock
+++ b/pixi.lock
@@ -86,7 +86,8 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/linux-64/mimalloc-3.0.1-h18b520e_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/mold-2.36.0-hff13881_1.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.0-h7b32b05_1.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/nushell-0.104.0-h0ed7cb8_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/patchelf-0.17.2-h58526e2_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/perl-5.32.1-7_hd590300_perl5.conda
@@ -199,7 +200,8 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/osx-64/mpc-1.3.1-h9d8efa1_1.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/mpfr-4.2.1-haed47dc_3.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.4.0-hc426f3f_1.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/nushell-0.104.0-h910683a_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.5.0-hc426f3f_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/patchelf-0.18.0-h93d8f39_1.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/perl-5.32.1-7_h10d778d_perl5.conda
@@ -315,7 +317,8 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mpc-1.3.1-h8f1351a_1.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mpfr-4.2.1-hb693164_3.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.0-h81ee809_1.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nushell-0.104.0-he48edd9_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.0-h81ee809_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/patchelf-0.18.0-h965bd2d_1.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.44-h297a79d_2.conda
@@ -408,8 +411,9 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/win-64/m2w64-libwinpthread-git-5.0.0.4634.697f757-2.tar.bz2
       - conda: https://conda.anaconda.org/conda-forge/win-64/make-4.3-h3d2af85_1.tar.bz2
       - conda: https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2
+      - conda: https://conda.anaconda.org/conda-forge/win-64/nushell-0.104.0-h3a84140_0.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/openmp-5.0.0-vc14_1.tar.bz2
-      - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.0-ha4e3fda_1.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.0-ha4e3fda_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.44-h3d7b363_2.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/perl-5.32.1.1-7_h57928b3_strawberry.conda
@@ -527,7 +531,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.13.0-pyhff2d567_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_2.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.3-h5fbd93e_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.0-h7b32b05_1.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/paginate-0.5.7-pyhd8ed1ab_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda
@@ -637,7 +641,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.13.0-pyhff2d567_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/openjpeg-2.5.3-h7fd6d84_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.4.0-hc426f3f_1.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.5.0-hc426f3f_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/paginate-0.5.7-pyhd8ed1ab_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda
@@ -742,7 +746,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.13.0-pyhff2d567_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.3-h8a3d83b_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.0-h81ee809_1.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.0-h81ee809_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/paginate-0.5.7-pyhd8ed1ab_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda
@@ -851,7 +855,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocstrings-python-1.13.0-pyhff2d567_0.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2
       - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.3-h4d64b90_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.0-ha4e3fda_1.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.0-ha4e3fda_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/paginate-0.5.7-pyhd8ed1ab_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda
@@ -927,7 +931,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_2.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.9.1-pyhd8ed1ab_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.0-h7b32b05_1.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-3.8.0-pyha770c72_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-hooks-4.6.0-pyhd8ed1ab_0.conda
@@ -967,7 +971,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/osx-64/libzlib-1.3.1-hd23fc13_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/ncurses-6.5-h0622a9a_2.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.9.1-pyhd8ed1ab_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.4.0-hc426f3f_1.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.5.0-hc426f3f_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-3.8.0-pyha770c72_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-hooks-4.6.0-pyhd8ed1ab_0.conda
@@ -1007,7 +1011,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_2.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.9.1-pyhd8ed1ab_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.0-h81ee809_1.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.0-h81ee809_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-3.8.0-pyha770c72_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-hooks-4.6.0-pyhd8ed1ab_0.conda
@@ -1044,7 +1048,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.48.0-h67fdade_1.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.9.1-pyhd8ed1ab_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.0-ha4e3fda_1.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.0-ha4e3fda_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-3.8.0-pyha770c72_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/pre-commit-hooks-4.6.0-pyhd8ed1ab_0.conda
@@ -4991,6 +4995,62 @@ packages:
   - pkg:pypi/nodeenv?source=hash-mapping
   size: 34574
   timestamp: 1734112236147
+- conda: https://conda.anaconda.org/conda-forge/linux-64/nushell-0.104.0-h0ed7cb8_0.conda
+  sha256: 88478a8c00c0f603b682e204c254e2df98537d7d36aebdb73ac89ca9835bcbe4
+  md5: 8b72f91da083c3edb445215224900d1b
+  depends:
+  - __glibc >=2.17,<3.0.a0
+  - libgcc >=13
+  - libstdcxx >=13
+  - libzlib >=1.3.1,<2.0a0
+  - openssl >=3.5.0,<4.0a0
+  constrains:
+  - __glibc >=2.17
+  license: MIT
+  license_family: MIT
+  size: 8864269
+  timestamp: 1746004904590
+- conda: https://conda.anaconda.org/conda-forge/osx-64/nushell-0.104.0-h910683a_0.conda
+  sha256: e2cd64ffe3835e9bcc8671e9ddf67ee29a31258873f8a83d54494e5e176b8286
+  md5: 77c8978598dbec942878a27111e1a8a7
+  depends:
+  - __osx >=10.13
+  - libcxx >=18
+  - libzlib >=1.3.1,<2.0a0
+  - openssl >=3.5.0,<4.0a0
+  constrains:
+  - __osx >=10.13
+  license: MIT
+  license_family: MIT
+  size: 8796353
+  timestamp: 1746005095512
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nushell-0.104.0-he48edd9_0.conda
+  sha256: 22482efbbd80095101b3934c1fa5a9157e7359e90bbdd4a3ef4ea45154010336
+  md5: f658d640c163ebd5ee2dbb47c601b269
+  depends:
+  - __osx >=11.0
+  - libcxx >=18
+  - libzlib >=1.3.1,<2.0a0
+  - openssl >=3.5.0,<4.0a0
+  constrains:
+  - __osx >=11.0
+  license: MIT
+  license_family: MIT
+  size: 8527833
+  timestamp: 1746005343847
+- conda: https://conda.anaconda.org/conda-forge/win-64/nushell-0.104.0-h3a84140_0.conda
+  sha256: f3985b024b336da91e3fdd8e867de6365a2e410d9f9a2f01810577268937213d
+  md5: 7eb4f556d641ed780652306c146f5438
+  depends:
+  - libzlib >=1.3.1,<2.0a0
+  - openssl >=3.5.0,<4.0a0
+  - ucrt >=10.0.20348.0
+  - vc >=14.2,<15
+  - vc14_runtime >=14.29.30139
+  license: MIT
+  license_family: MIT
+  size: 8128692
+  timestamp: 1746005743145
 - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.3-h5fbd93e_0.conda
   sha256: 5bee706ea5ba453ed7fd9da7da8380dd88b865c8d30b5aaec14d2b6dd32dbc39
   md5: 9e5816bc95d285c115a3ebc2f8563564
@@ -5058,43 +5118,40 @@ packages:
   license: NCSA
   purls: []
   size: 590466
-- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.0-h7b32b05_1.conda
-  sha256: f62f6bca4a33ca5109b6d571b052a394d836956d21b25b7ffd03376abf7a481f
-  md5: 4ce6875f75469b2757a65e10a5d05e31
+- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_1.conda
+  sha256: b4491077c494dbf0b5eaa6d87738c22f2154e9277e5293175ec187634bd808a0
+  md5: de356753cfdbffcde5bb1e86e3aa6cd0
   depends:
   - __glibc >=2.17,<3.0.a0
   - ca-certificates
   - libgcc >=13
   license: Apache-2.0
   license_family: Apache
-  purls: []
-  size: 2937158
-  timestamp: 1736086387286
-- conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.4.0-hc426f3f_1.conda
-  sha256: 879a960d586cf8a64131ac0c060ef575cfb8aa9f6813093cba92042a86ee867c
-  md5: eaae23dbfc9ec84775097898526c72ea
+  size: 3117410
+  timestamp: 1746223723843
+- conda: https://conda.anaconda.org/conda-forge/osx-64/openssl-3.5.0-hc426f3f_1.conda
+  sha256: bcac94cb82a458b4e3164da8d9bced08cc8c3da2bc3bd7330711a3689c1464a5
+  md5: 919faa07b9647beb99a0e7404596a465
   depends:
   - __osx >=10.13
   - ca-certificates
   license: Apache-2.0
   license_family: Apache
-  purls: []
-  size: 2590210
-  timestamp: 1736086530077
-- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.0-h81ee809_1.conda
-  sha256: 97772762abc70b3a537683ca9fc3ff3d6099eb64e4aba3b9c99e6fce48422d21
-  md5: 22f971393637480bda8c679f374d8861
+  size: 2739181
+  timestamp: 1746224401118
+- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.0-h81ee809_1.conda
+  sha256: 73d366c1597a10bcd5f3604b5f0734b31c23225536e03782c6a13f9be9d01bff
+  md5: 5c7aef00ef60738a14e0e612cfc5bcde
   depends:
   - __osx >=11.0
   - ca-certificates
   license: Apache-2.0
   license_family: Apache
-  purls: []
-  size: 2936415
-  timestamp: 1736086108693
-- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.4.0-ha4e3fda_1.conda
-  sha256: 519a06eaab7c878fbebb8cab98ea4a4465eafb1e9ed8c6ce67226068a80a92f0
-  md5: fb45308ba8bfe1abf1f4a27bad24a743
+  size: 3064197
+  timestamp: 1746223530698
+- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.0-ha4e3fda_1.conda
+  sha256: 02846553d2a4c9bde850c60824d0f02803eb9c9b674d5c1a8cce25bc387e748f
+  md5: 72c07e46b6766bb057018a9a74861b89
   depends:
   - ca-certificates
   - ucrt >=10.0.20348.0
@@ -5102,9 +5159,8 @@ packages:
   - vc14_runtime >=14.29.30139
   license: Apache-2.0
   license_family: Apache
-  purls: []
-  size: 8462960
-  timestamp: 1736088436984
+  size: 9025176
+  timestamp: 1746227349882
 - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda
   sha256: da157b19bcd398b9804c5c52fc000fcb8ab0525bdb9c70f95beaa0bb42f85af1
   md5: 3bfed7e6228ebf2f7b9eaa47f1b4e2aa
diff --git a/pixi.toml b/pixi.toml
index f47b2ffd..6ce350a0 100644
--- a/pixi.toml
+++ b/pixi.toml
@@ -40,6 +40,7 @@ requests = ">=2.32.2,<2.33"
 syrupy = "4.6.*"
 rust-src = ">=1.86.0,<1.87"
 boto3 = "*"
+nushell = "==0.104.0"
 
 [feature.lint.dependencies]
 actionlint = ">=1.7.4,<2"
diff --git a/py-rattler-build/Cargo.lock b/py-rattler-build/Cargo.lock
index 0b57f7d2..2d74a7a6 100644
--- a/py-rattler-build/Cargo.lock
+++ b/py-rattler-build/Cargo.lock
@@ -4130,6 +4130,7 @@ dependencies = [
  "tracing",
  "tracing-core",
  "tracing-subscriber",
+ "unicode-normalization",
  "url",
  "walkdir",
  "which",
diff --git a/src/packaging.rs b/src/packaging.rs
index 13dcf209..47cff1ba 100644
--- a/src/packaging.rs
+++ b/src/packaging.rs
@@ -1,9 +1,9 @@
 //! This module contains the functions to package a conda package from a given
 //! output.
 use std::{
-    collections::HashSet,
+    collections::{HashMap, HashSet},
     io::Write,
-    path::{Component, Path, PathBuf},
+    path::{Component, MAIN_SEPARATOR, Path, PathBuf},
 };
 
 use fs_err as fs;
@@ -15,6 +15,7 @@ use rattler_conda_types::{
 use rattler_package_streaming::write::{
     CompressionLevel, write_conda_package, write_tar_bz2_package,
 };
+use unicode_normalization::UnicodeNormalization;
 
 mod file_finder;
 mod file_mapper;
@@ -231,6 +232,140 @@ impl rattler_package_streaming::write::ProgressBar for ProgressBar {
     }
 }
 
+/// Error type for path normalization operations
+#[derive(Debug, thiserror::Error)]
+pub enum PathNormalizationError {
+    /// Error when a path component contains invalid Unicode
+    #[error("Path component contains invalid Unicode: {0}")]
+    InvalidUnicode(String),
+}
+
+/// Normalizes a component string for comparison.
+///
+/// This helper function applies Unicode normalization (NFKC) and optional case folding to a path component.
+/// When case folding is applied, it's done in a way that properly handles special Unicode cases.
+fn normalize_component(component_str: &str, to_lowercase: bool) -> String {
+    if to_lowercase {
+        let normalized = component_str.nfkc().collect::<String>();
+        normalized.to_uppercase().to_lowercase()
+    } else {
+        component_str.nfkc().collect::<String>()
+    }
+}
+
+/// Normalizes a path for case-insensitive comparison.
+///
+/// This function:
+/// 1. Applies Unicode normalization (NFKC) to each path component
+/// 2. Handles path separators consistently across platforms
+/// 3. Optionally converts to lowercase for case-insensitive comparison
+///
+/// Returns a normalized string representation of the path.
+fn normalize_path_for_comparison(
+    path: &Path,
+    to_lowercase: bool,
+) -> Result<String, PathNormalizationError> {
+    let estimated_capacity = path.as_os_str().len() * 6 / 5 + path.components().count();
+    let mut normalized = String::with_capacity(estimated_capacity);
+    let mut first = true;
+
+    for c in path.components() {
+        match c {
+            Component::CurDir => continue,
+            Component::RootDir => {
+                normalized.push(MAIN_SEPARATOR);
+                first = false;
+            }
+            _ => {
+                if !first {
+                    normalized.push(MAIN_SEPARATOR);
+                }
+
+                let os_str = match c {
+                    Component::Prefix(p) => p.as_os_str(),
+                    _ => c.as_os_str(),
+                };
+
+                let component_type = if matches!(c, Component::Prefix(_)) {
+                    "Prefix"
+                } else {
+                    "Component"
+                };
+                let component_str = os_str.to_str().ok_or_else(|| {
+                    PathNormalizationError::InvalidUnicode(format!(
+                        "{}: {}",
+                        component_type,
+                        os_str.to_string_lossy()
+                    ))
+                })?;
+
+                normalized.push_str(&normalize_component(component_str, to_lowercase));
+            }
+        }
+    }
+
+    Ok(normalized)
+}
+
+/// Finds paths that would collide on case-insensitive filesystems.
+///
+/// Returns groups of paths that differ only by case.
+pub fn find_case_insensitive_collisions<I, P>(paths: I) -> Vec<Vec<PathBuf>>
+where
+    I: IntoIterator<Item = P>,
+    P: AsRef<Path>,
+{
+    let paths_vec: Vec<P> = paths.into_iter().collect();
+    let mut lc_map: HashMap<String, Vec<PathBuf>> = HashMap::with_capacity(paths_vec.len() / 2);
+
+    for path_ref in &paths_vec {
+        let path = path_ref.as_ref();
+        let case_folded = match normalize_path_for_comparison(path, true) {
+            Ok(normalized) => normalized,
+            Err(err) => {
+                tracing::warn!(
+                    "Failed to normalize path for comparison: {}: {}",
+                    path.display(),
+                    err
+                );
+                path.display().to_string().to_lowercase()
+            }
+        };
+
+        lc_map
+            .entry(case_folded)
+            .or_insert_with(|| Vec::with_capacity(1))
+            .push(path.to_path_buf());
+    }
+
+    let mut result: Vec<Vec<PathBuf>> = lc_map
+        .into_values()
+        .filter(|group| group.len() > 1)
+        .map(|group| {
+            let mut unique_vec: Vec<PathBuf> = if group.len() <= 4 {
+                let mut unique = Vec::with_capacity(group.len());
+                for path in group {
+                    if !unique.iter().any(|p| p == &path) {
+                        unique.push(path);
+                    }
+                }
+                unique
+            } else {
+                let mut unique_set = HashSet::with_capacity(group.len());
+                group.into_iter().for_each(|path| {
+                    unique_set.insert(path);
+                });
+                unique_set.into_iter().collect()
+            };
+            unique_vec.sort_by(|a, b| a.as_os_str().cmp(b.as_os_str()));
+            unique_vec
+        })
+        .collect();
+
+    result.sort_by(|a, b| a[0].as_os_str().cmp(b[0].as_os_str()));
+    result
+}
+
 /// Given an output and a set of new files, create a conda package.
 /// This function will copy all the files to a temporary directory and then
 /// create a conda package from that. Note that the output needs to have its
@@ -309,6 +444,20 @@ pub fn package_conda(
         }
     });
 
+    for group in find_case_insensitive_collisions(&files) {
+        let list = group
+            .iter()
+            .map(|p| p.display().to_string())
+            .collect::<Vec<_>>()
+            .join(", ");
+        let warn_str = format!(
+            "Mixed-case filenames detected, case-insensitive filesystems may break: {}",
+            list
+        );
+        tracing::error!(warn_str);
+        output.record_warning(&warn_str);
+    }
+
     let normalize_path = |p: &Path| -> String { p.display().to_string().replace('\\', "/") };
 
     files.iter().for_each(|f| {
@@ -421,3 +570,45 @@ impl Output {
         package_conda(self, tool_configuration, &files_after)
     }
 }
+
+#[cfg(test)]
+mod packaging_tests {
+    use super::*;
+    use std::path::Path;
+
+    #[test]
+    fn test_find_case_insensitive_collisions_detects() {
+        let files = vec![
+            Path::new("foo/BAR"),
+            Path::new("foo/bar"),
+            Path::new("foo/Baz"),
+            Path::new("foo/qux"),
+        ];
+        let groups = find_case_insensitive_collisions(&files);
+        assert_eq!(groups.len(), 1);
+
+        let paths_in_group: Vec<String> =
+            groups[0].iter().map(|p| p.display().to_string()).collect();
+
+        assert!(paths_in_group.contains(&"foo/BAR".to_string()));
+        assert!(paths_in_group.contains(&"foo/bar".to_string()));
+    }
+
+    #[test]
+    fn test_find_case_insensitive_collisions_empty() {
+        let files = vec![Path::new("foo/bar"), Path::new("foo/baz")];
+        let groups = find_case_insensitive_collisions(&files);
+        assert!(groups.is_empty());
+    }
+
+    #[test]
+    fn test_find_case_insensitive_collisions_unicode() {
+        let files = vec![
+            Path::new("foo/straße"),
+            Path::new("foo/STRASSE"),
+            Path::new("foo/other"),
+        ];
+        let groups = find_case_insensitive_collisions(&files);
+        assert_eq!(groups.len(), 1);
+    }
+}
diff --git a/src/recipe/parser/script.rs b/src/recipe/parser/script.rs
index 94480d31..f6267fcd 100644
--- a/src/recipe/parser/script.rs
+++ b/src/recipe/parser/script.rs
@@ -8,6 +8,7 @@ use crate::{
 };
 use indexmap::IndexMap;
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use std::path::Path;
 use std::{borrow::Cow, path::PathBuf};
 
 /// Defines the script to run to build the package.
@@ -202,6 +203,42 @@ impl From<ScriptContent> for Script {
     }
 }
 
+/// Helper function to validate a path for invalid UTF-8 characters
+fn validate_path_utf8(
+    path: &Path,
+    span: &impl HasSpan,
+    field_name: &str,
+) -> Result<(), Vec<PartialParsingError>> {
+    if path.to_str().is_none() {
+        return Err(vec![_partialerror!(
+            *span.span(),
+            ErrorKind::InvalidValue((
+                field_name.to_string(),
+                "path contains invalid UTF-8 characters".into()
+            )),
+            help = "Ensure the path contains only valid UTF-8 characters"
+        )]);
+    }
+    Ok(())
+}
+
+/// Helper function to determine interpreter based on file extension
+fn determine_interpreter_from_path(path: &Path) -> Option<String> {
+    path.extension()
+        .and_then(|s| s.to_str())
+        .map(|ext| ext.to_lowercase())
+        .and_then(|ext_lower| match ext_lower.as_str() {
+            "py" => Some("python".to_string()),
+            "pl" => Some("perl".to_string()),
+            "r" => Some("rscript".to_string()),
+            "sh" | "bash" => Some("bash".to_string()),
+            "bat" | "cmd" => Some("cmd".to_string()),
+            "ps1" => Some("powershell".to_string()),
+            "nu" => Some("nushell".to_string()),
+            _ => None,
+        })
+}
+
 impl TryConvertNode<Script> for RenderedNode {
     fn try_convert(&self, name: &str) -> Result<Script, Vec<PartialParsingError>> {
         match self {
@@ -217,8 +254,16 @@ impl TryConvertNode<Script> for RenderedNode {
 }
 
 impl TryConvertNode<Script> for RenderedScalarNode {
-    fn try_convert(&self, _name: &str) -> Result<Script, Vec<PartialParsingError>> {
-        Ok(ScriptContent::CommandOrPath(self.source().to_owned()).into())
+    fn try_convert(&self, name: &str) -> Result<Script, Vec<PartialParsingError>> {
+        let path_str = self.source();
+        let path = PathBuf::from(path_str);
+
+        validate_path_utf8(&path, self, name)?;
+
+        let mut script: Script = ScriptContent::CommandOrPath(path_str.to_owned()).into();
+        script.interpreter = determine_interpreter_from_path(&path);
+
+        Ok(script)
     }
 }
 
@@ -232,7 +277,9 @@ impl TryConvertNode<Script> for RenderedSequenceNode {
             }
         }
 
-        if strings.len() == 1 {
+        if strings.is_empty() {
+            Ok(ScriptContent::Commands(strings).into())
+        } else if strings.len() == 1 {
             Ok(ScriptContent::CommandOrPath(strings[0].clone()).into())
         } else {
             Ok(ScriptContent::Commands(strings).into())
@@ -322,13 +369,21 @@ impl TryConvertNode<Script> for RenderedMappingNode {
             (None, None) => ScriptContent::Default,
         };
 
-        Ok(Script {
+        let mut script = Script {
             env,
             secrets,
             interpreter,
             content,
             cwd: None,
-        })
+        };
+
+        if script.interpreter.is_none() {
+            if let ScriptContent::Path(path) = &script.content {
+                script.interpreter = determine_interpreter_from_path(path);
+            }
+        }
+
+        Ok(script)
     }
 }
 
diff --git a/src/script/mod.rs b/src/script/mod.rs
index 2d336424..4e37f689 100644
--- a/src/script/mod.rs
+++ b/src/script/mod.rs
@@ -254,7 +254,8 @@ impl Script {
         //  executable is in a known place.
         let nushell_path = format!("bin/nu{}", std::env::consts::EXE_SUFFIX);
         let has_nushell = build_prefix
-            .map(|p| p.join(nushell_path))
+            .map(|p| p.join(&nushell_path))
+            .or_else(|| Some(run_prefix.join(&nushell_path)))
             .map(|p| p.is_file())
             .unwrap_or(false);
         if has_nushell {


Test patch:
diff --git a/test-data/recipes/interpreter-detection/bash/build.sh b/test-data/recipes/interpreter-detection/bash/build.sh
new file mode 100644
index 00000000..9ea934c7
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/bash/build.sh
@@ -0,0 +1,1 @@
+echo "Hello from Bash!"
diff --git a/test-data/recipes/interpreter-detection/bash/recipe.yaml b/test-data/recipes/interpreter-detection/bash/recipe.yaml
new file mode 100644
index 00000000..5a794866
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/bash/recipe.yaml
@@ -0,0 +1,9 @@
+package:
+  name: test-interpreter-bash
+  version: 0.1.0
+
+build:
+  script: build.sh
+
+tests:
+  - script: test.sh
diff --git a/test-data/recipes/interpreter-detection/bash/test.sh b/test-data/recipes/interpreter-detection/bash/test.sh
new file mode 100644
index 00000000..aca395f6
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/bash/test.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+echo "Hello from BASH!"
diff --git a/test-data/recipes/interpreter-detection/bat/build.bat b/test-data/recipes/interpreter-detection/bat/build.bat
new file mode 100644
index 00000000..fe4bb668
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/bat/build.bat
@@ -0,0 +1,1 @@
+@echo Hello from Cmd!
diff --git a/test-data/recipes/interpreter-detection/bat/recipe.yaml b/test-data/recipes/interpreter-detection/bat/recipe.yaml
new file mode 100644
index 00000000..18b0528d
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/bat/recipe.yaml
@@ -0,0 +1,9 @@
+package:
+  name: test-interpreter-bat
+  version: 0.1.0
+
+build:
+  script: build.bat
+
+tests:
+  - script: build.bat
diff --git a/test-data/recipes/interpreter-detection/nu/build.nu b/test-data/recipes/interpreter-detection/nu/build.nu
new file mode 100644
index 00000000..20283fed
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/nu/build.nu
@@ -0,0 +1,1 @@
+echo "Hello from Nushell!"
diff --git a/test-data/recipes/interpreter-detection/nu/recipe.yaml b/test-data/recipes/interpreter-detection/nu/recipe.yaml
new file mode 100644
index 00000000..a78b714d
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/nu/recipe.yaml
@@ -0,0 +1,16 @@
+package:
+  name: test-interpreter-nu
+  version: 0.1.0
+
+build:
+  script: build.nu
+
+requirements:
+  build:
+    - nushell
+
+tests:
+  - script: build.nu
+    requirements:
+      run:
+        - nushell
diff --git a/test-data/recipes/interpreter-detection/pl/build.pl b/test-data/recipes/interpreter-detection/pl/build.pl
new file mode 100644
index 00000000..09466ca4
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/pl/build.pl
@@ -0,0 +1,1 @@
+print "Hello from Perl!\n";
diff --git a/test-data/recipes/interpreter-detection/pl/recipe.yaml b/test-data/recipes/interpreter-detection/pl/recipe.yaml
new file mode 100644
index 00000000..43ab6b73
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/pl/recipe.yaml
@@ -0,0 +1,16 @@
+package:
+  name: test-interpreter-pl
+  version: 0.1.0
+
+build:
+  script: build.pl
+
+requirements:
+  build:
+    - perl
+
+tests:
+  - script: build.pl
+    requirements:
+      run:
+        - perl
diff --git a/test-data/recipes/interpreter-detection/py/build.py b/test-data/recipes/interpreter-detection/py/build.py
new file mode 100644
index 00000000..7243d25c
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/py/build.py
@@ -0,0 +1,1 @@
+print("Hello from Python!")
diff --git a/test-data/recipes/interpreter-detection/py/recipe.yaml b/test-data/recipes/interpreter-detection/py/recipe.yaml
new file mode 100644
index 00000000..81d2e718
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/py/recipe.yaml
@@ -0,0 +1,16 @@
+package:
+  name: test-interpreter-py
+  version: 0.1.0
+
+build:
+  script: build.py
+
+requirements:
+  build:
+    - python
+
+tests:
+  - script: build.py
+    requirements:
+      run:
+        - python
diff --git a/test-data/recipes/interpreter-detection/r/build.r b/test-data/recipes/interpreter-detection/r/build.r
new file mode 100644
index 00000000..5df436d2
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/r/build.r
@@ -0,0 +1,1 @@
+print("Hello from R!")
diff --git a/test-data/recipes/interpreter-detection/r/recipe.yaml b/test-data/recipes/interpreter-detection/r/recipe.yaml
new file mode 100644
index 00000000..76e2f0d0
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/r/recipe.yaml
@@ -0,0 +1,16 @@
+package:
+  name: test-interpreter-r
+  version: 0.1.0
+
+build:
+  script: build.r
+
+requirements:
+  build:
+    - r-base
+
+tests:
+  - script: build.r
+    requirements:
+      run:
+        - r-base
diff --git a/test-data/recipes/interpreter-detection/recipe.yaml b/test-data/recipes/interpreter-detection/recipe.yaml
new file mode 100644
index 00000000..d8999d04
--- /dev/null
+++ b/test-data/recipes/interpreter-detection/recipe.yaml
@@ -0,0 +1,29 @@
+recipe:
+  name: test-interpreter-all
+  version: 0.1.0
+
+build:
+  script:
+    - script: py/build.py
+    - script: pl/build.pl
+    - script: r/build.r
+
+outputs:
+    - package:
+        name: test-interpreter-all
+        version: 0.1.0
+      requirements:
+        build:
+          - python
+          - perl
+          - r-base
+          - nushell
+        run:
+          - perl
+          - r-base
+          - nushell
+      tests:
+        - script: py/build.py
+        - script: pl/build.pl
+        - script: r/build.r
+        - script: nu/build.nu
diff --git a/test/end-to-end/test_simple.py b/test/end-to-end/test_simple.py
index 411ffb07..8b5cdf0d 100644
--- a/test/end-to-end/test_simple.py
+++ b/test/end-to-end/test_simple.py
@@ -1380,6 +1380,68 @@ def test_abi3(rattler_build: RattlerBuild, recipes: Path, tmp_path: Path):
     assert index["platform"] == host_subdir().split("-")[0]
 
 
+@pytest.mark.skipif(
+    os.name == "nt" or platform.system() == "Darwin",
+    reason="Filesystem case-insensitivity prevents testing collision warning trigger",
+)
+def test_case_insensitive_collision_warning(
+    rattler_build: RattlerBuild, tmp_path: Path
+):
+    recipe_content = """
+context:
+  name: test-case-collision
+  version: 0.1.0
+
+package:
+  name: test-case-collision
+  version: 0.1.0
+
+build:
+  script:
+    # Create directories with case difference
+    - mkdir -p case_test
+    - echo "UPPER CASE FILE" > case_test/CASE-FILE.txt
+    - echo "lower case file" > case_test/case-file.txt
+    # Install the directory into the prefix to trigger packaging
+    - cp -r case_test $PREFIX/
+    # Add another test file to ensure packaging works
+    - echo "test content" > regular-file.txt
+    - cp regular-file.txt $PREFIX/
+
+about:
+  summary: A test package for case-insensitive file collisions
+"""
+    recipe_path = tmp_path / "recipe.yaml"
+    recipe_path.write_text(recipe_content)
+
+    args = rattler_build.build_args(
+        recipe_path,
+        tmp_path / "output",
+        extra_args=["-vvv"],
+    )
+
+    output = rattler_build(*args, stderr=STDOUT, text=True)
+    pkg = get_extracted_package(tmp_path / "output", "test-case-collision")
+    extracted_files_list = [str(f.relative_to(pkg)) for f in pkg.glob("**/*")]
+
+    assert (
+        "case_test/CASE-FILE.txt" in extracted_files_list
+    ), "CASE-FILE.txt not found in package"
+    assert (
+        "case_test/case-file.txt" in extracted_files_list
+    ), "case-file.txt not found in package"
+    assert (
+        "regular-file.txt" in extracted_files_list
+    ), "regular-file.txt not found in package"
+
+    collision_warning_pattern1 = "Mixed-case filenames detected, case-insensitive filesystems may break: case_test/CASE-FILE.txt, case_test/case-file.txt"
+    collision_warning_pattern2 = "Mixed-case filenames detected, case-insensitive filesystems may break: case_test/case-file.txt, case_test/CASE-FILE.txt"
+
+    assert (
+        collision_warning_pattern1 in output or collision_warning_pattern2 in output
+    ), f"Case collision warning not found in build output. Output contains:\n{output}"
+
+
 # This is how cf-scripts is using rattler-build - rendering recipes from stdin
 def test_rendering_from_stdin(
     rattler_build: RattlerBuild, recipes: Path, tmp_path: Path
@@ -1600,6 +1662,87 @@ def test_relative_file_loading(
     )
 
 
+@pytest.mark.parametrize(
+    "interpreter",
+    [
+        pytest.param(
+            "bash",
+            marks=pytest.mark.skipif(os.name == "nt", reason="bash only on unix"),
+        ),
+        pytest.param(
+            "bat",
+            marks=pytest.mark.skipif(os.name != "nt", reason="bat only on windows"),
+        ),
+        "py",
+        "pl",
+        "nu",
+        "r",
+    ],
+)
+def test_interpreter_detection(
+    rattler_build: RattlerBuild, recipes: Path, tmp_path: Path, interpreter: str
+):
+    """
+    Tests that rattler-build automatically detects the required interpreter
+    for build and test scripts based on their file extension, without explicit
+    interpreter specification in the recipe.
+    """
+    recipe_dir = recipes / "interpreter-detection" / interpreter
+    pkg_name = f"test-interpreter-{interpreter}"
+
+    try:
+        rattler_build.build(recipe_dir, tmp_path)
+    except CalledProcessError as e:
+        print(f"Build failed for interpreter: {interpreter}")
+        print(f"STDOUT:\n{e.stdout.decode() if e.stdout else ''}")
+        print(f"STDERR:\n{e.stderr.decode() if e.stderr else ''}")
+        raise
+
+    pkg_file = get_package(tmp_path, pkg_name)
+    assert pkg_file.exists()
+
+    test_output = rattler_build.test(pkg_file)
+
+    if interpreter == "bat":
+        expected_output = "Hello from Cmd!"
+    elif interpreter == "py":
+        expected_output = "Hello from Python!"
+    elif interpreter == "pl":
+        expected_output = "Hello from Perl!"
+    elif interpreter == "nu":
+        expected_output = "Hello from Nushell!"
+    elif interpreter == "r":
+        expected_output = "Hello from R!"
+    else:
+        expected_output = f"Hello from {interpreter.upper()}!"
+
+    assert expected_output in test_output
+    assert "all tests passed!" in test_output
+
+
+def test_interpreter_detection_all_tests(
+    rattler_build: RattlerBuild, recipes: Path, tmp_path: Path
+):
+    """
+    Tests that rattler-build can run multiple test scripts requiring
+    different interpreters within the same test phase.
+    """
+    recipe_dir = recipes / "interpreter-detection"
+    pkg_name = "test-interpreter-all"
+
+    rattler_build.build(recipe_dir, tmp_path)
+    pkg_file = get_package(tmp_path, pkg_name)
+    assert pkg_file.exists()
+
+    test_output = rattler_build.test(pkg_file)
+
+    assert "Hello from Python!" in test_output
+    assert "Hello from Perl!" in test_output
+    assert "Hello from R!" in test_output
+    assert "Hello from Nushell!" in test_output
+    assert "all tests passed!" in test_output
+
+
 def test_relative_git_path_py(
     rattler_build: RattlerBuild, recipes: Path, tmp_path: Path
 ):
@@ -1673,10 +1816,8 @@ def test_relative_git_path_py(
 package:
   name: test-relative-git
   version: 1.0.0
-
 source:
   git: ../../repo
-
 build:
   script:
     - if: unix