Skip to content
Merged
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
5 changes: 2 additions & 3 deletions Changes.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Unreleased
## 0.1.0 - 2026-10-01

- Added support for installing the experimental `sui-fork` binary (without debug builds).
- Fixed `suiup remove` treating directories as executables. Removal now checks for non-file paths before deleting binaries and saves completed removals even if a later deletion or default-file update fails.

- Fixed `suiup remove` aborting when an installed binary is missing from disk. Removal now clears missing entries and saves the updated installed and default binary lists.
- Changed `suiup remove` to preserve the active version and default executable. If the active version's stored file is missing, its installed entry is removed, but any existing default executable and its setting are kept.

Expand All @@ -14,7 +14,6 @@ The new binary version will become the default one automatically.

## 0.0.13 - 2026-04-07

- added support for installing the experimental `sui-fork` binary (without debug builds).
- fixed status command version comparison by using proper semver parsing.

## 0.0.12 - 2026-04-06
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Suiup - the installer and manager for CLI tools in the Sui ecosystem


`suiup` is a tool to install and manage different versions of CLI tools for working in the Sui ecosystem. It allows you to easily install and switch between different versions of `sui`, `sui-node`, `mvr`, `seal`, `walrus`, `site-builder`, `move-analyzer`, `ledger-signer`, and `yubikey-signer`.
`suiup` is a tool to install and manage different versions of CLI tools for working in the Sui ecosystem. It allows you to easily install and switch between different versions of `sui`, `sui-node`, `sui-fork`, `mvr`, `seal`, `walrus`, `site-builder`, `move-analyzer`, `ledger-signer`, and `yubikey-signer`.
After installation, run `suiup list` to find which binaries you can install. Check out the [Installation](#installation) to install and the [Quick Start](#quick-start) guide for how-to-use examples.

# Why suiup?
Expand All @@ -14,7 +14,7 @@ As new tools like [mvr](https://docs.suins.io/move-registry#using-mvr) and [walr

With `suiup`, you can:

- Install specific versions of `sui`, `sui-node`, `mvr`, `seal`, `walrus`, `site-builder`, `move-analyzer`, `ledger-signer`, and `yubikey-signer`
- Install specific versions of `sui`, `sui-node`, `sui-fork`, `mvr`, `seal`, `walrus`, `site-builder`, `move-analyzer`, `ledger-signer`, and `yubikey-signer`
- Install from a branch in the repository to try unreleased features or fixes
- Get debug builds of `sui` (needed for commands like `sui move test --coverage`)
- List and switch between installed binaries for different networks (e.g., devnet vs mainnet)
Expand All @@ -27,6 +27,7 @@ These are the binary names you can pass to `suiup install`:
| Binary | Example command | Source repository |
| ----------------- | --------------------------------------- | -------------------------------------------- |
| `sui` | `suiup install sui@testnet` | `MystenLabs/sui` |
| `sui-fork` | `suiup install sui-fork@testnet` | `MystenLabs/sui` |
| `mvr` | `suiup install mvr` | `MystenLabs/mvr` |
| `seal` | `suiup install seal` | `MystenLabs/seal` |
| `walrus` | `suiup install walrus@mainnet` | `MystenLabs/walrus` |
Expand Down Expand Up @@ -149,6 +150,17 @@ suiup install sui-node
suiup install sui-node@testnet-1.40.1 # install a specific version
```

### Install `sui-fork` (experimental)

```bash
suiup install sui-fork
suiup install sui-fork@testnet-1.81.0 # install a specific version
```

> [!WARNING]
> `sui-fork` is experimental. Breaking changes are expected, and it is not recommended for production
> workflows. Prebuilt debug versions are not available.

### List available binaries to install

```bash
Expand Down
13 changes: 13 additions & 0 deletions binaries/sui-fork.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name = "sui-fork"
description = "Sui Fork CLI - fork the Sui blockchain for local development and testing"
repository = "MystenLabs/sui"
main_branch = "main"
installation_type = "archive"
network_based = true
supported_networks = ["testnet", "devnet", "mainnet"]
default_network = "testnet"
supports_debug = false
cargo_package = ""
nightly_toolchain = ""
shared_repo_binary = true
experimental = true
15 changes: 15 additions & 0 deletions src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ mod tests {
for name in &[
"sui",
"sui-node",
"sui-fork",
"mvr",
"seal",
"walrus",
Expand Down Expand Up @@ -213,6 +214,20 @@ mod tests {
assert!(config.supported_networks.contains(&"mainnet".to_string()));
}

#[test]
fn sui_fork_config_values() {
let config = BinaryRegistry::global().get("sui-fork").unwrap();
assert_eq!(config.repository, "MystenLabs/sui");
assert_eq!(config.installation_type, InstallationType::Archive);
assert!(config.network_based);
assert_eq!(config.default_network, "testnet");
assert_eq!(config.supported_networks, ["testnet", "devnet", "mainnet"]);
assert!(!config.supports_debug);
assert!(config.shared_repo_binary);
assert!(config.experimental);
assert!(BinaryName::new("sui-fork").is_ok());
}

#[test]
fn mvr_config_values() {
let config = BinaryRegistry::global().get("mvr").unwrap();
Expand Down
26 changes: 8 additions & 18 deletions tests/commands_test.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

pub mod test_utils;

#[cfg(test)]
mod tests {
use crate::test_utils::TestEnv;
use anyhow::Result;
use std::fs;
use std::time::{Duration, SystemTime};
use suiup::commands::{CommandMetadata, parse_component_with_version};
use suiup::handlers::cleanup::handle_cleanup;
use suiup::paths;
use suiup::registry::BinaryName;
use suiup::set_env_var;
use tempfile::TempDir;

#[test]
fn test_parse_component_with_version() -> Result<(), anyhow::Error> {
Expand Down Expand Up @@ -76,8 +77,7 @@ mod tests {

#[test]
fn test_cleanup_empty_directory() -> Result<()> {
let temp_dir = TempDir::new()?;
set_env_var!("XDG_CACHE_HOME", temp_dir.path());
let _test_env = TestEnv::new()?;

// Test cleanup on empty directory
let result = handle_cleanup(false, 30, true);
Expand All @@ -88,8 +88,8 @@ mod tests {

#[test]
fn test_cleanup_dry_run() -> Result<()> {
let temp_dir = TempDir::new()?;
let cache_dir = temp_dir.path().join("suiup").join("release_archives");
let _test_env = TestEnv::new()?;
let cache_dir = paths::release_archive_dir();
fs::create_dir_all(&cache_dir)?;

// Create test files with different ages
Expand All @@ -103,8 +103,6 @@ mod tests {
let old_time = SystemTime::now() - Duration::from_secs(60 * 60 * 24 * 40); // 40 days ago
filetime::set_file_mtime(&old_file, filetime::FileTime::from_system_time(old_time))?;

set_env_var!("XDG_CACHE_HOME", temp_dir.path());

// Dry run should not remove files
let result = handle_cleanup(false, 30, true);
assert!(result.is_ok());
Expand All @@ -116,9 +114,7 @@ mod tests {

#[test]
fn test_cleanup_remove_old_files() -> Result<()> {
let temp_dir = TempDir::new()?;
// Set up environment variable for cache directory
set_env_var!("XDG_CACHE_HOME", temp_dir.path());
let _test_env = TestEnv::new()?;
// Create cache directory
let cache_dir = paths::release_archive_dir();
fs::create_dir_all(&cache_dir)?;
Expand All @@ -134,8 +130,6 @@ mod tests {
let old_time = SystemTime::now() - Duration::from_secs(60 * 60 * 24 * 40); // 40 days ago
filetime::set_file_mtime(&old_file, filetime::FileTime::from_system_time(old_time))?;

set_env_var!("XDG_CACHE_HOME", temp_dir.path());

// Actual cleanup should remove old file but keep new file
let result = handle_cleanup(false, 30, false);
assert!(result.is_ok());
Expand All @@ -147,9 +141,7 @@ mod tests {

#[test]
fn test_cleanup_remove_all() -> Result<()> {
let temp_dir = TempDir::new()?;
// Set up environment variable for cache directory
set_env_var!("XDG_CACHE_HOME", temp_dir.path());
let _test_env = TestEnv::new()?;
// Create cache directory
let cache_dir = paths::release_archive_dir();
fs::create_dir_all(&cache_dir)?;
Expand All @@ -161,8 +153,6 @@ mod tests {
fs::write(&file1, b"content1")?;
fs::write(&file2, b"content2")?;

set_env_var!("XDG_CACHE_HOME", temp_dir.path());

// Remove all should clear everything
let result = handle_cleanup(true, 30, false);
assert!(result.is_ok());
Expand Down
69 changes: 69 additions & 0 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,75 @@ mod tests {
Ok(())
}

#[test]
fn test_sui_fork_install_from_shared_archive() -> Result<()> {
let test_env = TestEnv::new()?;
let (os, arch) = detect_os_arch_for_tests();
let archive = format!("sui-testnet-v1.81.0-{os}-{arch}.tgz");
test_env.create_cached_archive(&archive, "sui-fork", b"sui-fork payload")?;

suiup_command(vec!["install", "sui-fork@testnet-v1.81.0", "-y"], &test_env)
.env("SUIUP_DEFAULT_BIN_DIR", &test_env.bin_dir)
.assert()
.success();

let suffix = std::env::consts::EXE_SUFFIX;
let installed_path = test_env
.data_dir
.join(format!("suiup/binaries/testnet/sui-fork-v1.81.0{suffix}"));
let default_path = test_env.bin_dir.join(format!("sui-fork{suffix}"));
assert_eq!(fs::read(&installed_path)?, b"sui-fork payload");
assert_eq!(fs::read(&default_path)?, b"sui-fork payload");
let installed: suiup::types::InstalledBinaries = serde_json::from_slice(&fs::read(
test_env.config_dir.join("suiup/installed_binaries.json"),
)?)?;
assert_eq!(installed.binaries().len(), 1);
let binary = &installed.binaries()[0];
assert_eq!(binary.binary_name, "sui-fork");
assert_eq!(binary.network_release, "testnet");
assert_eq!(binary.version, "v1.81.0");
assert!(!binary.debug);
assert_eq!(
binary.path.as_deref().map(std::path::Path::new),
Some(installed_path.as_path())
);

suiup_command(
vec!["default", "set", "sui-fork@testnet-v1.81.0"],
&test_env,
)
.env("SUIUP_DEFAULT_BIN_DIR", &test_env.bin_dir)
.assert()
.success();
assert_eq!(fs::read(default_path)?, b"sui-fork payload");
let defaults: serde_json::Value = serde_json::from_slice(&fs::read(
test_env.config_dir.join("suiup/default_version.json"),
)?)?;
assert_eq!(
defaults["sui-fork"],
serde_json::json!(["testnet", "v1.81.0", false])
);
Ok(())
}

#[test]
fn test_sui_fork_rejects_prebuilt_debug_versions() -> Result<()> {
let test_env = TestEnv::new()?;
for args in [
vec!["install", "sui-fork@testnet-v1.81.0", "--debug", "-y"],
vec!["default", "set", "sui-fork@testnet-v1.81.0", "--debug"],
] {
suiup_command(args, &test_env)
.env("SUIUP_DEFAULT_BIN_DIR", &test_env.bin_dir)
.assert()
.failure()
.stderr(predicate::str::contains(
"Debug flag is only available for the `sui` binary",
));
}
Ok(())
}

#[tokio::test]
async fn test_install_nightly() -> Result<()> {
Ok(())
Expand Down
74 changes: 43 additions & 31 deletions tests/test_utils.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use anyhow::{Result, anyhow};
use anyhow::Result;
use flate2::Compression;
use flate2::write::GzEncoder;
use std::env;
Expand Down Expand Up @@ -33,36 +33,20 @@ impl TestEnv {
let temp_dir = TempDir::new()?;
let base = temp_dir.path();

let home_dir = dirs::home_dir().ok_or_else(|| anyhow!("HOME directory is not set"))?;

let data_home = get_data_home();
let config_home = get_config_home();
let cache_home = get_cache_home();
let bin_home = get_default_bin_dir();

let data_dir = if let Ok(path) = data_home.strip_prefix(&home_dir) {
base.join(path)
} else {
base.join(data_home)
};

let config_dir = if let Ok(path) = config_home.strip_prefix(&home_dir) {
base.join(path)
} else {
base.join(config_home)
};

let cache_dir = if let Ok(path) = cache_home.strip_prefix(&home_dir) {
base.join(path)
} else {
base.join(cache_home)
};

let bin_dir = if let Ok(path) = bin_home.strip_prefix(&home_dir) {
base.join(path)
} else {
base.join(bin_home)
};
// Inherited absolute paths must never replace the temporary root.
#[cfg(windows)]
let data_dir = base.join("AppData").join("Local");
#[cfg(not(windows))]
let data_dir = base.join(".local").join("share");
#[cfg(windows)]
let config_dir = data_dir.clone();
#[cfg(not(windows))]
let config_dir = base.join(".config");
let cache_dir = base.join("cache");
#[cfg(windows)]
let bin_dir = data_dir.join("bin");
#[cfg(not(windows))]
let bin_dir = base.join(".local").join("bin");

// Create directories
std::fs::create_dir_all(&data_dir)?;
Expand All @@ -82,6 +66,8 @@ impl TestEnv {
"XDG_DATA_HOME",
"XDG_CONFIG_HOME",
"XDG_CACHE_HOME",
"TEMP",
"SUIUP_DEFAULT_BIN_DIR",
"PATH",
];

Expand All @@ -93,12 +79,16 @@ impl TestEnv {
// Set test env vars
#[cfg(windows)]
set_env_var!("LOCALAPPDATA", &data_dir); // it is the same for data and config
#[cfg(windows)]
set_env_var!("TEMP", &cache_dir);
#[cfg(not(windows))]
set_env_var!("XDG_DATA_HOME", &data_dir);
#[cfg(not(windows))]
set_env_var!("XDG_CONFIG_HOME", &config_dir);
#[cfg(not(windows))]
set_env_var!("XDG_CACHE_HOME", &cache_dir);
#[cfg(not(windows))]
set_env_var!("SUIUP_DEFAULT_BIN_DIR", &bin_dir);

// Add bin dir to PATH
let path = env::var("PATH").unwrap_or_default();
Expand Down Expand Up @@ -233,6 +223,28 @@ fn copy_cached_archive(src: &Path, dst: &Path) -> Result<()> {
Ok(())
}

#[test]
fn test_environment_paths_are_isolated() -> Result<()> {
let test_env = TestEnv::new()?;
for path in [
&test_env.data_dir,
&test_env.config_dir,
&test_env.cache_dir,
&test_env.bin_dir,
] {
assert!(
path.starts_with(test_env.temp_dir.path()),
"Test path escapes its temporary directory: {}",
path.display()
);
}
assert_eq!(get_data_home(), test_env.data_dir);
assert_eq!(get_config_home(), test_env.config_dir);
assert_eq!(get_cache_home(), test_env.cache_dir);
assert_eq!(get_default_bin_dir(), test_env.bin_dir);
Ok(())
}

// Mock HTTP client for testing
#[cfg(test)]
pub mod mock_http {
Expand Down
Loading