Repository: a-scie/jump
Pull request #283 (2025-03-09 19:26:55)

Support splitting out a subset of files.
The `SCIE=split` scie built-in command now supports listing a subset of
files to split out by passing them after `--`; e.g.:
`SCIE=split my-scie -- scie-jump` splits out just the scie-jump. For
discovery of what is available to split out, you can now run the split in
dry-run mode to get a preview of the files that would be split out
including their unpacked sizes.

Using `examples/node`:
```console
:; SCIE=split ./cowsay.js --dry-run
Would extract:

[destination file] [extracted size in bytes] [type] ([alt key for file])?
-------------------------------------------------------------------------
scie-jump 2042848 executable
node-v18.12.0-linux-x64.tar.xz 23692504 archive (node)
node_modules 1114908 directory
lift.json 982 blob
```

jsirois: This was prompted by https://github.com/pantsbuild/pants/issues/21100#issuecomment-2705295818
jsirois: I'm going to proceed with this to get out a scie-jump release and then science and Pex.

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 3902f1b..a1e64e1 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,14 @@
 # Release Notes
 
+## 1.6.0
+
+This release adds support for restricting `SCIE=split` to a subset of files in the scie as well as
+adding support for a `-n` / `--dry-run` mode.
+
+In addition, the bare `scie-jump` now displays a help message when executed without arguments and
+there is no `lift.json` manifest in the current directory. Help can also be requested via
+`-h` / `--help` and the `scie-jump` version can be displayed via `-V` / `--version`.
+
 ## 1.5.0
 
 This release adds support for Linux powerpc64le.
diff --git a/Cargo.lock b/Cargo.lock
index d35ccef..f4d66ee 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -66,6 +66,15 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
+dependencies = [
+ "derive_arbitrary",
+]
+
 [[package]]
 name = "autocfg"
 version = "1.1.0"
@@ -104,6 +113,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "bumpalo"
+version = "3.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+
 [[package]]
 name = "byteorder"
 version = "1.5.0"
@@ -182,8 +197,8 @@ checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
 dependencies = [
  "heck",
  "proc-macro2",
- "quote 1.0.28",
- "syn 2.0.18",
+ "quote 1.0.39",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -209,21 +224,18 @@ dependencies = [
 
 [[package]]
 name = "crc32fast"
-version = "1.3.2"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
 dependencies = [
  "cfg-if",
 ]
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.12"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
-dependencies = [
- "cfg-if",
-]
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
 
 [[package]]
 name = "crypto-common"
@@ -241,8 +253,19 @@ version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
 dependencies = [
- "quote 1.0.28",
- "syn 2.0.18",
+ "quote 1.0.39",
+ "syn 2.0.99",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
+dependencies = [
+ "proc-macro2",
+ "quote 1.0.39",
+ "syn 2.0.99",
 ]
 
 [[package]]
@@ -275,6 +298,17 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote 1.0.39",
+ "syn 2.0.99",
+]
+
 [[package]]
 name = "dotenvs"
 version = "0.1.0"
@@ -304,6 +338,12 @@ dependencies = [
  "log",
 ]
 
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
 [[package]]
 name = "errno"
 version = "0.2.8"
@@ -413,6 +453,12 @@ version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 
+[[package]]
+name = "hashbrown"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+
 [[package]]
 name = "heck"
 version = "0.5.0"
@@ -426,10 +472,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
 dependencies = [
  "autocfg",
- "hashbrown",
+ "hashbrown 0.12.3",
  "serde",
 ]
 
+[[package]]
+name = "indexmap"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.2",
+]
+
 [[package]]
 name = "io-lifetimes"
 version = "0.7.5"
@@ -479,7 +535,7 @@ dependencies = [
  "env_logger",
  "fd-lock",
  "flate2",
- "indexmap",
+ "indexmap 1.9.3",
  "itertools",
  "log",
  "logging_timer",
@@ -534,6 +590,12 @@ dependencies = [
  "scopeguard",
 ]
 
+[[package]]
+name = "lockfree-object-pool"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
+
 [[package]]
 name = "log"
 version = "0.4.26"
@@ -558,7 +620,7 @@ checksum = "27906ca51651609191eeb2d1fdc6b52b8024789ec188b07aad88b6dfbe392fbe"
 dependencies = [
  "log",
  "proc-macro2",
- "quote 1.0.28",
+ "quote 1.0.39",
  "syn 1.0.103",
 ]
 
@@ -713,9 +775,9 @@ checksum = "7be55bf0ae1635f4d7c7ddd6efc05c631e98a82104a73d35550bbc52db960027"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.78"
+version = "1.0.94"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
+checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
 dependencies = [
  "unicode-ident",
 ]
@@ -728,9 +790,9 @@ checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
 
 [[package]]
 name = "quote"
-version = "1.0.28"
+version = "1.0.39"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
+checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801"
 dependencies = [
  "proc-macro2",
 ]
@@ -752,7 +814,7 @@ checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
 dependencies = [
  "getrandom 0.2.8",
  "redox_syscall",
- "thiserror",
+ "thiserror 1.0.37",
 ]
 
 [[package]]
@@ -847,7 +909,7 @@ dependencies = [
 
 [[package]]
 name = "scie-jump"
-version = "1.5.0"
+version = "1.6.0"
 dependencies = [
  "bstr",
  "env_logger",
@@ -882,7 +944,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
 dependencies = [
  "proc-macro2",
- "quote 1.0.28",
+ "quote 1.0.39",
  "syn 1.0.103",
 ]
 
@@ -908,6 +970,12 @@ dependencies = [
  "digest",
 ]
 
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
 [[package]]
 name = "smallvec"
 version = "1.10.0"
@@ -948,18 +1016,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
 dependencies = [
  "proc-macro2",
- "quote 1.0.28",
+ "quote 1.0.39",
  "unicode-ident",
 ]
 
 [[package]]
 name = "syn"
-version = "2.0.18"
+version = "2.0.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
+checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2"
 dependencies = [
  "proc-macro2",
- "quote 1.0.28",
+ "quote 1.0.39",
  "unicode-ident",
 ]
 
@@ -994,7 +1062,16 @@ version = "1.0.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
 dependencies = [
- "thiserror-impl",
+ "thiserror-impl 1.0.37",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl 2.0.12",
 ]
 
 [[package]]
@@ -1004,10 +1081,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
 dependencies = [
  "proc-macro2",
- "quote 1.0.28",
+ "quote 1.0.39",
  "syn 1.0.103",
 ]
 
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote 1.0.39",
+ "syn 2.0.99",
+]
+
 [[package]]
 name = "tuple"
 version = "0.5.1"
@@ -1418,14 +1506,33 @@ dependencies = [
 
 [[package]]
 name = "zip"
-version = "0.6.6"
+version = "2.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
+checksum = "b280484c454e74e5fff658bbf7df8fdbe7a07c6b2de4a53def232c15ef138f3a"
 dependencies = [
- "byteorder",
+ "arbitrary",
  "crc32fast",
  "crossbeam-utils",
+ "displaydoc",
  "flate2",
+ "indexmap 2.7.1",
+ "memchr",
+ "thiserror 2.0.12",
+ "zopfli",
+]
+
+[[package]]
+name = "zopfli"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
+dependencies = [
+ "bumpalo",
+ "crc32fast",
+ "lockfree-object-pool",
+ "log",
+ "once_cell",
+ "simd-adler32",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 0944440..0ae1d0c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,7 @@ members = [
 
 [package]
 name = "scie-jump"
-version = "1.5.0"
+version = "1.6.0"
 description = "The self contained interpreted executable launcher."
 authors = [
     "John Sirois <john.sirois@gmail.com>",
@@ -27,7 +27,7 @@ logging_timer = "1.1"
 tempfile = "3.18"
 
 [workspace.dependencies.zip]
-version = "0.6"
+version = "2.2"
 default-features = false
 # We specifically don't include "time" which is otherwise default. Without time, zip creation
 # always uses 1/1/1980 and gives us reproducible zip creation by default.
diff --git a/jump/src/archive.rs b/jump/src/archive.rs
index e386ace..53e360a 100644
--- a/jump/src/archive.rs
+++ b/jump/src/archive.rs
@@ -7,18 +7,18 @@ use std::path::{Path, PathBuf};
 use log::debug;
 use logging_timer::time;
 use walkdir::WalkDir;
-use zip::write::FileOptions;
+use zip::write::SimpleFileOptions;
 
 #[cfg(not(target_family = "unix"))]
-pub fn create_options(_metadata: &Metadata) -> Result<FileOptions, String> {
-    Ok(FileOptions::default())
+pub fn create_options(_metadata: &Metadata) -> Result<SimpleFileOptions, String> {
+    Ok(SimpleFileOptions::default())
 }
 
 #[cfg(target_family = "unix")]
-pub fn create_options(metadata: &Metadata) -> Result<FileOptions, String> {
+pub fn create_options(metadata: &Metadata) -> Result<SimpleFileOptions, String> {
     use std::os::unix::fs::PermissionsExt;
     let perms = metadata.permissions();
-    Ok(FileOptions::default().unix_permissions(perms.mode()))
+    Ok(SimpleFileOptions::default().unix_permissions(perms.mode()))
 }
 
 fn create_zip(dir: &Path) -> Result<PathBuf, String> {
diff --git a/jump/src/config.rs b/jump/src/config.rs
index 86ef994..0a61e14 100644
--- a/jump/src/config.rs
+++ b/jump/src/config.rs
@@ -329,7 +329,7 @@ impl Config {
         Ok(config)
     }
 
-    pub fn serialize<W: Write>(&self, mut stream: W, fmt: Fmt) -> Result<(), String> {
+    pub fn serialize<W: Write>(&self, stream: &mut W, fmt: Fmt) -> Result<(), String> {
         let mut write_bytes = |bytes| {
             stream
                 .write_all(bytes)
diff --git a/jump/src/context.rs b/jump/src/context.rs
index de0d285..5570b95 100644
--- a/jump/src/context.rs
+++ b/jump/src/context.rs
@@ -84,7 +84,7 @@ impl LiftManifest {
     fn install(&self) -> Result<(), String> {
         atomic_path(&self.path, Target::File, |path| {
             config(self.jump.clone(), self.lift.clone()).serialize(
-                std::fs::OpenOptions::new()
+                &mut std::fs::OpenOptions::new()
                     .write(true)
                     .create_new(true)
                     .open(path)
diff --git a/jump/src/installer.rs b/jump/src/installer.rs
index 1fe6911..096d8ce 100644
--- a/jump/src/installer.rs
+++ b/jump/src/installer.rs
@@ -1,12 +1,13 @@
 // Copyright 2022 Science project contributors.
 // Licensed under the Apache License, Version 2.0 (see LICENSE).
 
+use std::fmt::Debug;
 use std::fs::{OpenOptions, Permissions};
 use std::io::{Cursor, Read, Seek};
 use std::path::Path;
 
 use logging_timer::time;
-use tempfile::TempDir;
+use zip::ZipArchive;
 
 use crate::atomic::{atomic_path, Target};
 use crate::config::{ArchiveType, Compression, FileType};
@@ -245,42 +246,19 @@ impl<'a> Installer<'a> {
                     0
                 }
                 FileEntry::ScieTote((tote_file, entries)) => {
-                    let mut scie_tote: Option<TempDir> = None;
-                    let mut scie_tote_src = || {
-                        if let Some(tempdir) = scie_tote.as_ref() {
-                            return Ok::<_, String>(tempdir.path().join(&tote_file.name));
-                        }
-                        let scie_tote_tmpdir = TempDir::new().map_err(|e| {
+                    let mut scie_tote = ZipArchive::new(Cursor::new(
+                        &self.payload[location..(location + tote_file.size)],
+                    ))
+                    .map_err(|e| format!("Failed to load scie-tote {tote_file:?}: {e}"))?;
+
+                    for (file, dst) in entries {
+                        let entry = scie_tote.by_name_seek(&file.name).map_err(|e| {
                             format!(
-                                "Failed to create a temporary directory to extract the scie-tote \
-                                to: {e}"
+                                "Failed to find {name} in the scie-tote: {e}",
+                                name = file.name
                             )
                         })?;
-                        let path = scie_tote_tmpdir.path().join(&tote_file.name);
-                        let bytes = &self.payload[location..(location + tote_file.size)];
-                        unpack(
-                            tote_file.file_type,
-                            tote_file.executable.unwrap_or(false),
-                            || Ok((Cursor::new(bytes), ())),
-                            tote_file.hash.as_str(),
-                            &path,
-                        )?;
-                        scie_tote = Some(scie_tote_tmpdir);
-                        Ok(path)
-                    };
-
-                    for (file, dst) in entries {
-                        let file_src = || {
-                            let scie_tote_path = scie_tote_src()?;
-                            let src_path = scie_tote_path.join(&file.name);
-                            let file = std::fs::File::open(&src_path).map_err(|e| {
-                                format!(
-                                    "Failed to open {file:?} at {src} from the unpacked scie-tote: {e}",
-                                    src = src_path.display()
-                                )
-                            })?;
-                            Ok((file, ()))
-                        };
+                        let file_src = || Ok((entry, ()));
                         unpack(
                             file.file_type,
                             file.executable.unwrap_or(false),
diff --git a/jump/src/lib.rs b/jump/src/lib.rs
index 87baca7..56f703f 100644
--- a/jump/src/lib.rs
+++ b/jump/src/lib.rs
@@ -79,10 +79,11 @@ install (-s|--symlink) [dest dir]*
 
 list: List the names of the commands contained in this scie.
 
-split [directory]?
+split (-n|--dry-run) [directory]? [-- [file]*]?
 
     Split this scie into its component files in the given directory or
-    else the current directory if no argument is given.
+    else the current directory if no argument is given. To just split out
+    certain files, list their names or ids after `--`.
 ",
         boot_pack_help = BOOT_PACK_HELP.split('\n').join("\n    ")
     )
diff --git a/package/Cargo.toml b/package/Cargo.toml
index 37d62ed..1d959be 100644
--- a/package/Cargo.toml
+++ b/package/Cargo.toml
@@ -5,7 +5,7 @@ description = "Packages the scie-jump binary into its final form."
 authors = [
     "John Sirois <john.sirois@gmail.com>",
 ]
-edition = "2021"
+edition = "2024"
 publish = false
 
 [dependencies]
diff --git a/src/boot.rs b/src/boot.rs
index 20dd0f9..2671c45 100644
--- a/src/boot.rs
+++ b/src/boot.rs
@@ -28,7 +28,7 @@ pub(crate) fn inspect(jump: Jump, lift: Lift) -> ExitResult {
     let config = jump::config(jump, lift);
     let fmt = Fmt::new().pretty(true).trailing_newline(true);
     config
-        .serialize(std::io::stdout(), fmt)
+        .serialize(&mut std::io::stdout(), fmt)
         .map_err(|e| Code::FAILURE.with_message(format!("Failed to serialize lift manifest: {e}")))
 }
 
diff --git a/src/boot/pack.rs b/src/boot/pack.rs
index 3fcc27a..4835150 100644
--- a/src/boot/pack.rs
+++ b/src/boot/pack.rs
@@ -209,7 +209,7 @@ fn pack(
             })?;
         };
     }
-    if let Some(tote) = scie_tote.as_mut() {
+    if let Some(mut tote) = scie_tote {
         tote.zip_writer
             .finish()
             .map_err(|e| format!("Failed to finalize the scie-tote zip: {e}"))?;
@@ -249,7 +249,7 @@ fn pack(
         .pretty(!single_line)
         .leading_newline(true)
         .trailing_newline(true);
-    config.serialize(binary, fmt).map_err(|e| {
+    config.serialize(&mut binary, fmt).map_err(|e| {
         format!(
             "Failed to serialize the lift manifest to {binary}: {e}",
             binary = binary_path.display()
diff --git a/src/boot/split.rs b/src/boot/split.rs
index 6350d23..56720b4 100644
--- a/src/boot/split.rs
+++ b/src/boot/split.rs
@@ -1,14 +1,15 @@
 // Copyright 2022 Science project contributors.
 // Licensed under the Apache License, Version 2.0 (see LICENSE).
 
-use std::env;
+use std::collections::HashSet;
+use std::fmt::Debug;
 use std::fs::Permissions;
-use std::io::Read;
+use std::io::{Read, Seek, SeekFrom, Write};
 use std::path::{Path, PathBuf};
+use std::{cmp, env, io};
 
 use jump::config::{FileType, Fmt};
 use jump::{File, Jump, Lift, Source};
-use log::debug;
 use proc_exit::{Code, Exit, ExitResult};
 use zip::ZipArchive;
 
@@ -36,156 +37,416 @@ fn executable_permissions() -> Option<Permissions> {
     Some(Permissions::from_mode(0o755))
 }
 
+struct ChosenFiles {
+    files: HashSet<String>,
+}
+
+impl ChosenFiles {
+    fn new() -> Self {
+        Self {
+            files: HashSet::new(),
+        }
+    }
+
+    fn add(&mut self, file: String) {
+        self.files.insert(file);
+    }
+
+    fn is_empty(&self) -> bool {
+        self.files.is_empty()
+    }
+
+    fn contains(&self, name: &str) -> bool {
+        self.files.is_empty() || self.files.contains(name)
+    }
+
+    fn selected<'b, 'a: 'b>(&'a self, file: &'b File) -> Option<Option<&'b String>> {
+        if self.files.is_empty() {
+            Some(None)
+        } else if self.files.contains(file.name.as_str()) {
+            Some(Some(&file.name))
+        } else if file
+            .key
+            .as_ref()
+            .map(|key| self.files.contains(key))
+            .unwrap_or(false)
+        {
+            Some(file.key.as_ref())
+        } else {
+            None
+        }
+    }
+}
+
 pub(crate) fn split(jump: Jump, mut lift: Lift, scie_path: PathBuf) -> ExitResult {
-    let base = if let Some(base) = env::args().nth(1) {
-        PathBuf::from(base)
-    } else {
-        env::current_dir().map_err(|e| {
-            Code::FAILURE.with_message(format!(
-                "No target directory for the split was passed and the current directory could not \
-                be determined: {e}"
-            ))
-        })?
-    };
-    std::fs::create_dir_all(&base).map_err(|e| {
-        Code::FAILURE.with_message(format!(
-            "Failed to create target directory {base} for split: {e}",
-            base = base.display()
-        ))
-    })?;
+    let mut extra_args_seen = false;
+    let mut dry_run = false;
+    let mut chosen_files = ChosenFiles::new();
+    let mut custom_base: Option<PathBuf> = None;
+    for arg in env::args().skip(1) {
+        match arg.as_str() {
+            "-n" | "--dry-run" if !extra_args_seen => dry_run = true,
+            "--" => extra_args_seen = true,
+            _ if extra_args_seen => {
+                chosen_files.add(arg);
+            }
+            path => {
+                if let Some(custom) = custom_base {
+                    return Err(Code::FAILURE.with_message(format!(
+                        "Cannot split to {path} in addition to {custom}. Only one split \
+                                dir is allowed.",
+                        custom = custom.display()
+                    )));
+                } else {
+                    custom_base = Some(PathBuf::from(path));
+                }
+            }
+        }
+    }
+
+    let base = custom_base.unwrap_or_default();
 
-    let scie = std::fs::File::open(&scie_path).map_err(|e| {
+    let mut scie = std::fs::File::open(&scie_path).map_err(|e| {
         Code::FAILURE.with_message(format!(
             "Failed to open scie at {scie_path} for splitting: {e}",
             scie_path = scie_path.display()
         ))
     })?;
 
-    let mut dst = std::fs::OpenOptions::new()
-        .write(true)
-        .create_new(true)
-        .open(
-            base.join("scie-jump")
-                .with_extension(std::env::consts::EXE_EXTENSION),
-        )
-        .map_err(|e| {
-            Code::FAILURE.with_message(format!("Failed to open scie-jump for extraction: {e}"))
-        })?;
-    if let Some(permissions) = executable_permissions() {
-        dst.set_permissions(permissions).map_err(|e| {
+    let scie_jump_path = base
+        .join("scie-jump")
+        .with_extension(std::env::consts::EXE_EXTENSION);
+
+    if dry_run {
+        eprintln!("Would extract:");
+        eprintln!();
+        eprintln!("[destination file] [extracted size in bytes] [type] ([alt key for file])?");
+        eprintln!("-------------------------------------------------------------------------");
+        io::stderr().lock().flush().ok();
+    } else {
+        std::fs::create_dir_all(&base).map_err(|e| {
             Code::FAILURE.with_message(format!(
-                "Failed to open file metadata for the scie-jump: {e}"
+                "Failed to create target directory {base} for split: {e}",
+                base = base.display()
             ))
         })?;
     }
-    let mut src = scie
-        .try_clone()
-        .map_err(|e| Code::FAILURE.with_message(format!("Failed to dup scie handle: {e}")))?
-        .take(jump.size as u64);
-    std::io::copy(&mut src, &mut dst)
-        .map_err(|e| Code::FAILURE.with_message(format!("Failed to extract scie-jump: {e}")))?;
 
-    let mut scie_tote = vec![];
-    let scie_tote_index = lift.files.len() - 1;
-    for (index, file) in lift.files.iter().enumerate() {
-        if file.source != Source::Scie {
-            continue;
-        } else if file.size == 0 {
-            scie_tote.push(file);
-        } else if file.file_type == FileType::Directory
-            || (index == scie_tote_index && !scie_tote.is_empty())
-        {
-            let mut src = scie
-                .try_clone()
-                .map_err(|e| Code::FAILURE.with_message(format!("Failed to dup scie handle: {e}")))?
-                .take(file.size as u64);
-            let mut zip_file = tempfile::tempfile().map_err(|e| {
-                Code::FAILURE.with_message(format!(
-                    "Failed to create a temporary file to extract {file} to: {e}",
-                    file = file.name
-                ))
-            })?;
-            std::io::copy(&mut src, &mut zip_file).map_err(|e| {
-                Code::FAILURE.with_message(format!(
-                    "Failed to extract {file} zip to a temp file: {e}",
-                    file = file.name
-                ))
-            })?;
-            let mut zip_archive = ZipArchive::new(zip_file).map_err(|e| {
-                Code::FAILURE
-                    .with_message(format!("Failed to open {file} zip: {e}", file = file.name))
-            })?;
-            let dst = if file.file_type == FileType::Directory {
-                ensure_parent_dir(&base, file)?
-            } else {
-                base.to_path_buf()
-            };
-            debug!("Extracting {file:?} to {dst}...", dst = dst.display());
-            zip_archive.extract(&dst).map_err(|e| {
-                Code::FAILURE.with_message(format!(
-                    "Failed to extract scie-tote to {base}: {e}",
-                    base = base.display()
-                ))
-            })?;
+    if chosen_files.contains("scie-jump") {
+        if dry_run {
+            println!(
+                "{path} {size} executable",
+                path = scie_jump_path.display(),
+                size = jump.size
+            );
         } else {
-            let dst = ensure_parent_dir(&base, file)?;
-            let mut out = std::fs::OpenOptions::new()
+            let mut dst = std::fs::OpenOptions::new()
                 .write(true)
                 .create_new(true)
-                .open(&dst)
+                .open(scie_jump_path)
                 .map_err(|e| {
-                    Code::FAILURE.with_message(format!(
-                        "Failed to open {dst} for extraction: {e}",
-                        dst = dst.display()
-                    ))
+                    Code::FAILURE
+                        .with_message(format!("Failed to open scie-jump for extraction: {e}"))
                 })?;
-            let file_size = file.size as u64;
+            mark_executable(&mut dst)?;
             let mut src = scie
                 .try_clone()
                 .map_err(|e| Code::FAILURE.with_message(format!("Failed to dup scie handle: {e}")))?
-                .take(file_size);
-            std::io::copy(&mut src, &mut out).map_err(|e| {
-                Code::FAILURE.with_message(format!(
-                    "Failed to extract {file:?} to {dst}: {e}",
-                    dst = dst.display()
-                ))
+                .take(jump.size as u64);
+            std::io::copy(&mut src, &mut dst).map_err(|e| {
+                Code::FAILURE.with_message(format!("Failed to extract scie-jump: {e}"))
             })?;
         }
     }
 
-    if !scie_tote.is_empty() {
-        lift.files.remove(lift.files.len() - 1);
-        for file in lift.files.iter_mut() {
-            if file.source == Source::Scie {
-                let metadata = base.join(&file.name).metadata().map_err(|e| {
-                    Code::FAILURE.with_message(format!(
-                        "Failed to determine size of {file}: {e}",
-                        file = file.name
-                    ))
-                })?;
-                file.size = metadata.len() as usize;
+    let mut have_scie_tote = false;
+    let scie_tote_index = lift.files.len() - 1;
+    let mut offset = jump.size;
+    for (index, file) in lift.files.iter().enumerate() {
+        if file.source != Source::Scie {
+            continue;
+        } else if file.size == 0 {
+            have_scie_tote = true;
+        } else if (file.file_type == FileType::Directory && chosen_files.selected(file).is_some())
+            || (index == scie_tote_index && have_scie_tote)
+        {
+            let mut zip_archive = open_embedded_zip(&mut scie, offset as u64, file)?;
+            if chosen_files.is_empty()
+                || (file.file_type == FileType::Directory && chosen_files.selected(file).is_some())
+            {
+                if dry_run && file.file_type == FileType::Directory {
+                    print_directory_entry(&base, &zip_archive, file);
+                } else if dry_run {
+                    for file in lift.files.iter() {
+                        if file.size == 0 && file.source == Source::Scie {
+                            print!(
+                                "{path} {size} {file_type}",
+                                path = base.join(&file.name).display(),
+                                size = zip_archive
+                                    .by_name(&file.name)
+                                    .map_err(|e| Code::FAILURE.with_message(format!(
+                                        "Expected to find {file} in the scie-tote: {e}",
+                                        file = file.name
+                                    )))?
+                                    .size(),
+                                file_type = file_type(file)
+                            );
+                            if let Some(key) = &file.key {
+                                print!(" ({key})");
+                            }
+                            println!();
+                        }
+                    }
+                } else {
+                    let dst = if file.file_type == FileType::Directory {
+                        ensure_parent_dir(&base, file)?
+                    } else {
+                        base.to_path_buf()
+                    };
+                    zip_archive.extract(&dst).map_err(|e| {
+                        Code::FAILURE.with_message(format!(
+                            "Failed to extract scie-tote to {base}: {e}",
+                            base = base.display()
+                        ))
+                    })?;
+                }
+            } else {
+                for file in lift.files.iter() {
+                    if let Some(maybe_selected_file) = chosen_files.selected(file) {
+                        let selected_file =
+                            maybe_selected_file.expect("Split files were selected.");
+                        let mut src = zip_archive.by_name(file.name.as_str()).map_err(|e| {
+                            Code::FAILURE.with_message(format!(
+                                "The selected file {selected_file} could not be found in this \
+                                scie: {e}"
+                            ))
+                        })?;
+                        if dry_run {
+                            print!(
+                                "{path} {size} {file_type}",
+                                path = base.join(&file.name).display(),
+                                size = src.size(),
+                                file_type = file_type(file)
+                            );
+                            if let Some(key) = &file.key {
+                                print!(" ({key})");
+                            }
+                            println!();
+                        } else {
+                            extract_to(&base, file, &mut src)?;
+                        }
+                    }
+                }
+            }
+        } else if chosen_files.selected(file).is_some() {
+            if dry_run && file.file_type == FileType::Directory {
+                let zip_archive = open_embedded_zip(&mut scie, offset as u64, file)?;
+                print_directory_entry(&base, &zip_archive, file);
+            } else if dry_run {
+                print!(
+                    "{path} {size} {file_type}",
+                    path = base.join(&file.name).display(),
+                    size = file.size,
+                    file_type = file_type(file)
+                );
+                if let Some(key) = &file.key {
+                    print!(" ({key})");
+                }
+                println!();
+            } else {
+                let file_size = file.size as u64;
+                let mut reader = scie
+                    .try_clone()
+                    .map_err(|e| {
+                        Code::FAILURE.with_message(format!("Failed to dup scie handle: {e}"))
+                    })?
+                    .take(file_size);
+                extract_to(&base, file, &mut reader)?;
+            }
+        }
+        offset += file.size;
+    }
+
+    if chosen_files.contains("lift.json") {
+        if have_scie_tote {
+            let scie_tote = lift.files.remove(scie_tote_index);
+            let start = scie.seek(SeekFrom::Start(jump.size as u64)).map_err(|e| {
+                Code::FAILURE.with_message(format!("Failed to seek to scie-tote: {e}"))
+            })?;
+            let mut zip_archive = open_embedded_zip(&mut scie, start, &scie_tote)?;
+            for file in lift.files.iter_mut() {
+                if file.size == 0 && file.source == Source::Scie {
+                    file.size = zip_archive
+                        .by_name(&file.name)
+                        .map_err(|e| {
+                            Code::FAILURE.with_message(format!(
+                                "Expected to find {file} in the scie-tote: {e}",
+                                file = file.name
+                            ))
+                        })?
+                        .size() as usize;
+                }
             }
         }
+
+        if dry_run {
+            let mut writer = io::Cursor::new(vec![]);
+            serialize_manifest(jump, lift, &mut writer)?;
+            println!(
+                "{file} {size} blob",
+                file = base.join("lift.json").display(),
+                size = writer.position()
+            );
+        } else {
+            serialize_manifest(
+                jump,
+                lift,
+                &mut std::fs::OpenOptions::new()
+                    .write(true)
+                    .create_new(true)
+                    .open(base.join("lift.json"))
+                    .map_err(|e| {
+                        Code::FAILURE
+                            .with_message(format!("Failed to open lift manifest for writing: {e}"))
+                    })?,
+            )?;
+        };
+    }
+
+    Code::SUCCESS.ok()
+}
+
+fn file_type(file: &File) -> &str {
+    match file.file_type {
+        FileType::Archive(_) => "archive",
+        FileType::Blob if file.executable.unwrap_or_default() => "executable",
+        FileType::Blob => "blob",
+        FileType::Directory => "directory",
     }
+}
 
-    let manifest = std::fs::OpenOptions::new()
+fn print_directory_entry<P: AsRef<Path>, R>(base: P, zip_archive: &ZipArchive<R>, file: &File) {
+    // N.B.: If file is a Directory then we store it in the scie as a zip and file.size represents
+    // the compressed size of the zipped up directory. We want the uncompressed size of the zipped
+    // up directory here as the proper fair warning on a split, which will extract the directory
+    // loose.
+    print!(
+        "{path} {size} {file_type}",
+        path = base.as_ref().join(&file.name).display(),
+        size = zip_archive.decompressed_size().unwrap_or(file.size as u128),
+        file_type = file_type(file)
+    );
+    if let Some(key) = &file.key {
+        print!(" ({key})");
+    }
+    println!();
+}
+
+fn extract_to<P: AsRef<Path>, R: Read>(base: P, file: &File, reader: &mut R) -> Result<u64, Exit> {
+    let dst = ensure_parent_dir(base.as_ref(), file)?;
+    let mut out = std::fs::OpenOptions::new()
         .write(true)
         .create_new(true)
-        .open(base.join("lift.json"))
+        .open(&dst)
         .map_err(|e| {
-            Code::FAILURE.with_message(format!("Failed to open lift manifest for writing: {e}"))
+            Code::FAILURE.with_message(format!(
+                "Failed to open {dst} for extraction: {e}",
+                dst = dst.display()
+            ))
         })?;
+    if file.executable.unwrap_or_default() {
+        mark_executable(&mut out)?;
+    }
+    std::io::copy(reader, &mut out).map_err(|e| {
+        Code::FAILURE.with_message(format!(
+            "Failed to extract {file} to {dst}: {e}",
+            file = file.name,
+            dst = dst.display()
+        ))
+    })
+}
+
+fn mark_executable(file: &mut std::fs::File) -> Result<(), Exit> {
+    if let Some(permissions) = executable_permissions() {
+        file.set_permissions(permissions).map_err(|e| {
+            Code::FAILURE.with_message(format!(
+                "Failed to open file metadata for the scie-jump: {e}"
+            ))
+        })
+    } else {
+        Ok(())
+    }
+}
+
+struct EmbeddedZipReader<'a, S> {
+    scie: &'a mut S,
+    start: u64,
+    length: u64,
+    offset: u64,
+}
+
+impl<'a, S> EmbeddedZipReader<'a, S> {
+    pub fn new(scie: &'a mut S, start: u64, length: u64) -> Self {
+        Self {
+            scie,
+            start,
+            length,
+            offset: 0,
+        }
+    }
+}
+
+impl<R: Read + Debug> Read for EmbeddedZipReader<'_, R> {
+    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
+        let written = self.scie.take(self.length - self.offset).read(buf)?;
+        self.offset += written as u64;
+        Ok(written)
+    }
+}
+
+impl<S: Seek + Debug> Seek for EmbeddedZipReader<'_, S> {
+    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
+        let offset = match pos {
+            SeekFrom::Start(offset) => Some(offset),
+            SeekFrom::End(offset) => self.length.checked_add_signed(offset),
+            SeekFrom::Current(offset) => self.offset.checked_add_signed(offset),
+        };
+        match offset {
+            None => Err(io::Error::new(
+                io::ErrorKind::InvalidInput,
+                "invalid seek to a negative or overflowing position",
+            )),
+            Some(offset) => {
+                let clamped_offset = cmp::min(self.length, offset);
+                let new_inner_offset = self
+                    .scie
+                    .seek(SeekFrom::Start(self.start + clamped_offset))?;
+                self.offset = new_inner_offset - self.start;
+                Ok(self.offset)
+            }
+        }
+    }
+}
+
+fn open_embedded_zip<'a, S: Seek + Read + Debug>(
+    scie: &'a mut S,
+    start: u64,
+    file: &File,
+) -> Result<ZipArchive<EmbeddedZipReader<'a, S>>, Exit> {
+    let length = file.size as u64;
+    ZipArchive::new(EmbeddedZipReader::<'a, S>::new(scie, start, length)).map_err(|e| {
+        Code::FAILURE.with_message(format!("Failed to open {file} zip: {e}", file = file.name))
+    })
+}
+
+fn serialize_manifest<W: Write>(jump: Jump, lift: Lift, writer: &mut W) -> Result<(), Exit> {
     jump::config(jump, lift)
         .serialize(
-            manifest,
+            writer,
             Fmt::new()
                 .pretty(true)
                 .leading_newline(false)
                 .trailing_newline(true),
         )
-        .map_err(|e| {
-            Code::FAILURE.with_message(format!("Failed to serialize lift manifest: {e}"))
-        })?;
-
-    Code::SUCCESS.ok()
+        .map_err(|e| Code::FAILURE.with_message(format!("Failed to serialize lift manifest: {e}")))
 }


Test patch:
diff --git a/examples/cat/test.sh b/examples/cat/test.sh
index 760e022..600a9cd 100644
--- a/examples/cat/test.sh
+++ b/examples/cat/test.sh
@@ -20,7 +20,7 @@ gc "${PWD}/${JAVA}.sha256"
 
 ./"${JAVA}" "scie-jump boot-pack"
 gc "${PWD}/split"
-SCIE="split" ./"${JAVA}" split
+SCIE="split" ./"${JAVA}" split -- scie-jump
 
 # N.B.: For unknown reasons, macOS tail -1 split/scie-jump* includes garbage; i.e.: more than just
 # the last line. This, though, works.
diff --git a/examples/load/test.sh b/examples/load/test.sh
index 8dd2c50..836f0bb 100644
--- a/examples/load/test.sh
+++ b/examples/load/test.sh
@@ -10,6 +10,21 @@ check_cmd mktemp
 gc "${PWD}/cowsay"
 "${SCIE_JUMP}" "${LIFT}"
 
+# N.B.: The file size differences here are due to CRLF line endings on git checkout under Windows.
+split_dry_run_output="$(SCIE="split" ./cowsay -n -- get.sh)"
+if [ "${OS}" == "windows" ] && [ "get.sh 332 blob" != "${split_dry_run_output}" ]; then
+  die "
+Expected a split dry run to indicate get.sh is a blob on Windows. Got:\n${split_dry_run_output}"
+elif [ "${OS}" != "windows" ] && [ "get.sh 319 executable" != "${split_dry_run_output}" ]; then
+  die "
+Expected a split dry run to indicate get.sh is executable on Unix. Got:\n${split_dry_run_output}"
+fi
+gc "${PWD}/split"
+SCIE="split" ./cowsay split -- get.sh
+if [ "${OS}" != "windows" ] && [ ! -x split/get.sh ]; then
+  die "The get.sh script should retain its executable bit on Unix."
+fi
+
 # Force downloads to occur to exercise the load functionality even if nce cache has the JDK and the
 # cowsay jars already from other examples.
 SCIE_BASE="$(mktemp -d)"