Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/create_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,9 @@ jobs:
- name: Package CLI binary
run: |
mv ${{ steps.bundle_cli.outputs.binary_path }} oz-${{ steps.get-config.outputs.channel }}
tar czf oz-${{ steps.get-config.outputs.channel }}-macos-${{ matrix.arch }}.tar.gz oz-${{ steps.get-config.outputs.channel }} -C "$(dirname "${{ steps.bundle_cli.outputs.bundled_resources_dir }}")" resources
tar czf oz-${{ steps.get-config.outputs.channel }}-macos-${{ matrix.arch }}.tar.gz \
oz-${{ steps.get-config.outputs.channel }} \
-C "$(dirname "${{ steps.bundle_cli.outputs.bundled_resources_dir }}")" resources lib

- name: Add CLI to GitHub release assets
if: ${{ needs.prepare_release.outputs.should_publish == 'true' }}
Expand Down
26 changes: 26 additions & 0 deletions crates/remote_server/src/install_remote_server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,29 @@ cleanup() {
}
trap cleanup EXIT

install_lib_dir() {
extracted_lib_dir="$1"
staged_lib_dir="$install_dir/.lib.$(basename "$tmpdir")"
backup_lib_dir="$install_dir/.lib.previous.$(basename "$tmpdir")"

rm -rf "$staged_lib_dir" "$backup_lib_dir"
mv "$extracted_lib_dir" "$staged_lib_dir"

if [ -e "$install_dir/lib" ]; then
mv "$install_dir/lib" "$backup_lib_dir"
fi

if mv "$staged_lib_dir" "$install_dir/lib"; then
rm -rf "$backup_lib_dir"
else
if [ -e "$backup_lib_dir" ]; then
mv "$backup_lib_dir" "$install_dir/lib"
fi
rm -rf "$staged_lib_dir"
exit 1
fi
}

staging_tarball_path="{staging_tarball_path}"
if [ -n "$staging_tarball_path" ]; then
# SCP fallback: tarball already uploaded by the client.
Expand Down Expand Up @@ -82,4 +105,7 @@ tar -xzf "$tmpdir/oz.tar.gz" -C "$tmpdir"
bin=$(find "$tmpdir" -type f -name 'oz*' ! -name '*.tar.gz' | head -n1)
if [ -z "$bin" ]; then echo "no binary found in tarball" >&2; exit 1; fi
chmod +x "$bin"
if [ -d "$tmpdir/lib" ]; then
install_lib_dir "$tmpdir/lib"
fi
mv "$bin" "$install_dir/{binary_name}{version_suffix}"
127 changes: 127 additions & 0 deletions crates/remote_server/src/setup_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,133 @@ fn install_script_avoids_pattern_substitution_for_tilde_expansion() {
);
}

/// Regression: macOS CLI tarballs may include Swift runtime libraries
/// in a `lib/` sidecar next to the standalone binary. The installer
/// must preserve the binary's existing final path while installing the
/// sidecar at `$install_dir/lib` using the production shell script.
#[cfg(unix)]
#[test]
fn install_script_installs_lib_sidecar_without_changing_binary_path() {
use command::blocking::Command;
use std::{
fs,
io::Write,
path::{Path, PathBuf},
process::Stdio,
time::{SystemTime, UNIX_EPOCH},
};

struct TempDir {
path: PathBuf,
}

impl TempDir {
fn new() -> Self {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after UNIX epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!(
"warp-remote-install-lib-sidecar-{}-{nanos}",
std::process::id()
));
fs::create_dir_all(&path).expect("failed to create temp dir");
Self { path }
}

fn path(&self) -> &Path {
&self.path
}
}

impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}

let temp = TempDir::new();
let fake_home = temp.path().join("home");
let package_dir = temp.path().join("package");
let lib_dir = package_dir.join("lib");
fs::create_dir_all(&lib_dir).expect("failed to create package lib dir");

let binary_path = package_dir.join("oz-test");
let mut binary = fs::File::create(&binary_path).expect("failed to create package binary");
writeln!(binary, "#!/bin/sh").expect("failed to write package binary");
writeln!(binary, "echo test").expect("failed to write package binary");

fs::write(lib_dir.join("libswiftCore.dylib"), "swift").expect("failed to write dylib");

let tarball_path = temp.path().join("oz.tar.gz");
let tar_output = Command::new("tar")
.arg("-czf")
.arg(&tarball_path)
.arg("-C")
.arg(&package_dir)
.arg(".")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("failed to run tar");
assert!(
tar_output.status.success(),
"tar failed with {:?}: stderr={}",
tar_output.status,
String::from_utf8_lossy(&tar_output.stderr),
);

fs::create_dir_all(&fake_home).expect("failed to create fake home");
let script = install_script(Some(
tarball_path
.to_str()
.expect("temp tarball path should be valid UTF-8"),
));

let bash = if Path::new("/bin/bash").exists() {
"/bin/bash"
} else {
"bash"
};
let output = Command::new(bash)
.arg("-c")
.arg(&script)
.env("HOME", &fake_home)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("failed to run install script");
assert!(
output.status.success(),
"install script failed with {:?}: stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr),
);

let installed_binary = remote_server_binary().replacen(
'~',
fake_home
.to_str()
.expect("fake home path should be valid UTF-8"),
1,
);
assert!(
Path::new(&installed_binary).is_file(),
"expected binary at unchanged remote-server path: {installed_binary}",
);

let installed_lib = remote_server_dir().replacen(
'~',
fake_home
.to_str()
.expect("fake home path should be valid UTF-8"),
1,
) + "/lib/libswiftCore.dylib";
assert!(
Path::new(&installed_lib).is_file(),
"expected Swift sidecar dylib at {installed_lib}",
);
}
#[test]
fn version_hash_is_deterministic() {
// version_hash uses the compile-time GIT_RELEASE_TAG which is typically
Expand Down
57 changes: 57 additions & 0 deletions script/macos/bundle
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,53 @@ function relpath() {
python -c "import os,sys;print(os.path.relpath(*(sys.argv[1:])))" "$@";
}

function add_rpath_if_missing() {
local binary_path="$1"
local rpath="$2"

if otool -l "$binary_path" | awk -v rpath="$rpath" '
/LC_RPATH/ { in_rpath = 1; next }
in_rpath && $1 == "path" {
if ($2 == rpath) { found = 1 }
in_rpath = 0
}
END { exit found ? 0 : 1 }
'; then
echo "$binary_path already has rpath $rpath"
else
echo "Adding rpath $rpath to $binary_path"
install_name_tool -add_rpath "$rpath" "$binary_path"
fi
}

function copy_swift_stdlibs_for_cli() {
local binary_path="$1"
local destination="$2"
local swift_stdlib_tool_output
swift_stdlib_tool_output="$(mktemp -d -t swift-stdlib-tool-XXXXXXXXXX)"

rm -rf "$destination"

mkdir -p "$destination"
echo "Copying Swift runtime libraries required by $binary_path into $destination"
xcrun swift-stdlib-tool \
--copy \
--scan-executable "$binary_path" \
--platform macosx \
--destination "$swift_stdlib_tool_output"

find "$swift_stdlib_tool_output" -type f -name 'libswift*.dylib' -maxdepth 3 -print0 | while IFS= read -r -d '' dylib_path; do
cp -p "$dylib_path" "$destination/"
done
# Do not rewrite /usr/lib/swift load commands to @rpath. Xcode's x86_64
# backward-deployment Swift 5.0 dylibs are only valid before macOS 10.14.4
# and abort on newer macOS when loaded directly. Keeping the system Swift
# load commands preserves modern runtime behavior while still packaging the
# stdlibs swift-stdlib-tool reports for the archive layout.

rm -rf "$swift_stdlib_tool_output"
}

# Defaults for command-line flags.
UNIVERSAL_BINARY=true
BUILD_BINARY=true
Expand Down Expand Up @@ -587,6 +634,10 @@ elif [[ "$ARTIFACT" == "cli" ]]; then

echo "Copying binary into $OUT_DIR/$WARP_BIN"
cp "target/$DEFAULT_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN" "$OUT_DIR/$WARP_BIN"
# Prefer system Swift libraries on macOS versions that provide them, and fall
# back to the sidecar lib directory on older systems.
add_rpath_if_missing "$OUT_DIR/$WARP_BIN" "/usr/lib/swift"
add_rpath_if_missing "$OUT_DIR/$WARP_BIN" "@executable_path/lib"

if [[ -n "$TARGET_ARCH" && -e "target/$DEFAULT_TARGET/$TARGET_PROFILE_DIR/$WARP_BIN.dSYM" ]]; then
echo "Copying .dSYM into $OUT_DIR/$WARP_BIN.dSYM"
Expand All @@ -596,6 +647,8 @@ elif [[ "$ARTIFACT" == "cli" ]]; then
echo "Preparing CLI resources directory"
BUNDLED_RESOURCES_DIR="$OUT_DIR/resources"
"$WORKSPACE_ROOT_DIR/script/prepare_bundled_resources" "$BUNDLED_RESOURCES_DIR" "$RELEASE_CHANNEL" "$CARGO_PROFILE"
CLI_SWIFT_STDLIB_DIR="$OUT_DIR/lib"
copy_swift_stdlibs_for_cli "$OUT_DIR/$WARP_BIN" "$CLI_SWIFT_STDLIB_DIR"


# Set the primary binary path to output.
Expand Down Expand Up @@ -840,6 +893,10 @@ if [ "${GITHUB_ACTIONS}" == "true" ]; then
if [[ -n "$BUNDLED_RESOURCES_DIR" ]]; then
echo "bundled_resources_dir=$BUNDLED_RESOURCES_DIR" >> "$GITHUB_OUTPUT"
fi

if [[ -n "$CLI_SWIFT_STDLIB_DIR" ]]; then
echo "cli_swift_stdlib_dir=$CLI_SWIFT_STDLIB_DIR" >> "$GITHUB_OUTPUT"
fi

echo "::echo::off"
fi
Expand Down
Loading