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
20 changes: 20 additions & 0 deletions docs/lang/rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,26 @@ mise use -g rust@beta
cargo build
```

Use the rolling nightly channel:

```sh
mise use -g rust@nightly
cargo build
```

The configuration remains `nightly`, while mise resolves the current Rust channel manifest to a concrete
`nightly-YYYY-MM-DD` toolchain for installation and lockfiles. This keeps the configured channel rolling while making
locked installs reproducible. Run `mise upgrade rust` or `mise lock --bump` to advance the locked nightly.

To keep a specific nightly instead, configure its date explicitly:

```sh
mise use -g rust@nightly-2026-08-13
```

An explicitly dated nightly is an exact pin. Commands using `--bump`, such as `mise upgrade --bump rust`, can replace
that pin with the current nightly.

Use a specific version of rust:

```sh
Expand Down
182 changes: 182 additions & 0 deletions e2e/core/test_rust_nightly_lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#!/usr/bin/env bash

set -euo pipefail

export MISE_LOCKFILE=1
export MISE_CARGO_HOME="$PWD/cargo"
export MISE_RUSTUP_HOME="$PWD/rustup"

SERVER_ROOT="$PWD/server"
PORT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/mise-rust-nightly.XXXXXX")
PORT_FILE="$PORT_DIR/port"
MANIFEST="$SERVER_ROOT/dist/channel-rust-nightly.toml"
mkdir -p "$SERVER_ROOT/dist" "$SERVER_ROOT/repos/rust-lang/rust" "$MISE_CARGO_HOME/bin" "$MISE_RUSTUP_HOME"
touch "$MISE_RUSTUP_HOME/settings.toml"
printf '[]\n' >"$SERVER_ROOT/repos/rust-lang/rust/releases"

write_manifest() {
cat >"$MANIFEST" <<EOF
manifest-version = "2"
date = "$1"
EOF
}

write_manifest 2026-08-12

cat >"$MISE_CARGO_HOME/bin/rustup" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

echo "$*" >>"$CARGO_HOME/rustup.log"
case "${1:-} ${2:-}" in
"toolchain install")
mkdir -p "$RUSTUP_HOME/toolchains/$3"
;;
"toolchain uninstall")
;;
"component list" | "target list")
;;
*)
echo "unexpected rustup args: $*" >&2
exit 1
;;
esac
EOF

cat >"$MISE_CARGO_HOME/bin/rustc" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
test -d "$RUSTUP_HOME/toolchains/$RUSTUP_TOOLCHAIN"
echo "rustc $RUSTUP_TOOLCHAIN"
EOF

cat >"$MISE_CARGO_HOME/bin/cargo" <<'EOF'
#!/usr/bin/env bash
echo cargo
EOF

chmod +x "$MISE_CARGO_HOME/bin/rustup" "$MISE_CARGO_HOME/bin/rustc" "$MISE_CARGO_HOME/bin/cargo"

python3 - "$SERVER_ROOT" "$PORT_FILE" <<'PY' &
import http.server
import os
import socketserver
import sys

root, port_file = sys.argv[1:]
os.chdir(root)
socketserver.TCPServer.allow_reuse_address = True
with socketserver.TCPServer(("127.0.0.1", 0), http.server.SimpleHTTPRequestHandler) as httpd:
with open(port_file, "w") as file:
file.write(str(httpd.server_address[1]))
httpd.serve_forever()
PY
SERVER_PID=$!
cleanup() {
kill "$SERVER_PID" 2>/dev/null || true
rm -rf "$PORT_DIR"
}
trap cleanup EXIT

wait_for_file "$PORT_FILE" "Rust nightly manifest HTTP server port" 30 "$SERVER_PID"
SERVER_PORT=$(cat "$PORT_FILE")

cat >mise.toml <<EOF
[settings]
lockfile = true
url_replacements = { "https://static.rust-lang.org" = "http://127.0.0.1:$SERVER_PORT", "https://api.github.com" = "http://127.0.0.1:$SERVER_PORT" }

[tools]
rust = "dev"

[tool_alias.rust.versions]
dev = "nightly"
EOF
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

# A legacy literal-nightly install is not a reproducible fallback for offline use,
# including when the rolling channel is reached through an alias.
mkdir -p "$MISE_DATA_DIR/installs/rust"
ln -s "$MISE_CARGO_HOME/bin" "$MISE_DATA_DIR/installs/rust/nightly"
assert_fail_contains "MISE_OFFLINE=1 mise exec -- rustc -V 2>&1" "cannot resolve rolling channel rust@nightly while offline"

cat >mise.toml <<EOF
[settings]
lockfile = true
url_replacements = { "https://static.rust-lang.org" = "http://127.0.0.1:$SERVER_PORT", "https://api.github.com" = "http://127.0.0.1:$SERVER_PORT" }

[tools]
rust = "nightly"

[tool_alias.rust.versions]
dev = "nightly"
EOF

# Locking resolves the rolling channel without installing it or changing config.
mise lock
assert_contains "cat mise.lock" 'version = "nightly-2026-08-12"'
assert_not_contains "cat mise.lock" 'version = "nightly"'
assert "test ! -e '$MISE_CARGO_HOME/rustup.log'"
assert_contains "cat mise.toml" 'rust = "nightly"'

# Explicit tool filtering follows the same concrete-resolution path.
rm mise.lock
mise lock rust@nightly
assert_contains "cat mise.lock" 'version = "nightly-2026-08-12"'
assert_not_contains "cat mise.lock" 'version = "nightly"'

# Explicit aliases are resolved before rolling-channel detection.
rm mise.lock
mise lock rust@dev
assert_contains "cat mise.lock" 'version = "nightly-2026-08-12"'
assert_not_contains "cat mise.lock" 'version = "dev"'

mise install
assert_contains "cat '$MISE_CARGO_HOME/rustup.log'" "toolchain install nightly-2026-08-12"
assert "MISE_OFFLINE=1 mise exec -- rustc -V" "rustc nightly-2026-08-12"

# Advancing the manifest makes the channel outdated and upgrade updates the lock,
# while the configured selector remains rolling.
write_manifest 2026-08-13
assert_contains "mise outdated rust" "nightly-2026-08-13"
mise upgrade rust --no-prune
assert_contains "cat mise.lock" 'version = "nightly-2026-08-13"'
assert_contains "cat mise.toml" 'rust = "nightly"'
assert_contains "cat '$MISE_CARGO_HOME/rustup.log'" "toolchain install nightly-2026-08-13"

# An explicit dated nightly remains pinned unless --bump is requested.
cat >mise.toml <<EOF
[settings]
lockfile = true
url_replacements = { "https://static.rust-lang.org" = "http://127.0.0.1:$SERVER_PORT", "https://api.github.com" = "http://127.0.0.1:$SERVER_PORT" }

[tools]
rust = "nightly-2026-08-12"
EOF
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
write_manifest 2026-08-14
assert_not_contains "mise outdated rust" "nightly-2026-08-14"
assert_contains "mise outdated --bump rust" "nightly-2026-08-14"
mise upgrade --bump rust --no-prune
assert_contains "cat mise.toml" 'rust = "nightly-2026-08-14"'
assert_contains "cat '$MISE_CARGO_HOME/rustup.log'" "toolchain install nightly-2026-08-14"

# A malformed manifest must not replace a valid existing lock entry.
cat >mise.toml <<EOF
[settings]
lockfile = true
url_replacements = { "https://static.rust-lang.org" = "http://127.0.0.1:$SERVER_PORT", "https://api.github.com" = "http://127.0.0.1:$SERVER_PORT" }

[tools]
rust = "nightly"
EOF
cp mise.lock mise.lock.before
printf 'manifest-version = "2"\ndate = "invalid"\n' >"$MANIFEST"
assert_fail_contains "mise lock --bump 2>&1" "invalid Rust nightly channel manifest date"
assert "cmp mise.lock.before mise.lock"

# A broken nightly manifest does not prevent listing stable Rust releases.
mise cache clear rust
assert_contains "mise ls-remote --no-versions-host rust" "stable"

write_manifest 2026-08-14
mise cache clear rust
assert_contains "mise ls-remote --no-versions-host rust" "nightly-2026-08-14"
19 changes: 19 additions & 0 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2239,6 +2239,12 @@ pub trait Backend: Debug + Send + Sync {
Ok(None)
}

/// Whether an opaque version string should use [`Backend::resolve_exact_version`]
/// even when it does not have the usual dotted release shape.
fn is_exact_version(&self, _version: &str) -> bool {
false
}

/// Whether `version` names a rolling release channel (e.g. zig's "master")
/// rather than a concrete version. Cheap (no network). Channels are re-resolved
/// to a concrete version like "latest" so `mise upgrade`/`outdated` can track
Expand Down Expand Up @@ -2270,6 +2276,13 @@ pub trait Backend: Debug + Send + Sync {
Ok(None)
}

/// Whether a rolling channel must be resolved to a concrete version before it
/// can be used. Backends that opt in fail in offline mode when neither a lock
/// entry nor a concrete installed channel build is available.
fn requires_concrete_channel_version(&self, _version: &str) -> bool {
false
}

/// Backend opt-in for installing an unresolved `latest` request.
///
/// Most backends must resolve `latest` to a concrete version before install.
Expand Down Expand Up @@ -3860,6 +3873,12 @@ pub trait Backend: Debug + Send + Sync {
Ok(None)
}

/// Whether [`Backend::outdated_info`] fully replaces the generic outdated
/// resolver rather than supplementing it.
fn uses_custom_outdated_info(&self) -> bool {
false
}

// ========== Lockfile Metadata Fetching Methods ==========

/// Optional: Provide tarball URL for platform-specific tool installation
Expand Down
1 change: 1 addition & 0 deletions src/cli/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ impl Install {
resolve_options: ResolveOptions {
use_locked_version: true,
latest_versions: true,
resolve_rolling_channels: false,
prefer_exact_version: false,
before_date: self.get_before_date()?,
before_date_from_default: false,
Expand Down
41 changes: 27 additions & 14 deletions src/cli/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,16 +180,11 @@ impl Lock {
filter_installed_versions_by_release_date: true,
latest_versions: self.bump,
use_locked_version: !self.bump,
// Lock moving channels to their current concrete value without making
// ordinary `latest` requests ignore an installed concrete version.
resolve_rolling_channels: true,
..Default::default()
};
let has_configured_release_age = settings.minimum_release_age.is_some()
|| config
.get_tool_request_set()
.await?
.tools
.values()
.flatten()
.any(|request| request.options().minimum_release_age().is_some());
let monorepo_union = if !self.global && config.monorepo_lockfile_root().is_some() {
Some(config.monorepo_union().await?)
} else {
Expand All @@ -210,12 +205,10 @@ impl Lock {
.await?;
ts_owned = monorepo_ts;
&ts_owned
} else if self.bump || before_date.is_some() || has_configured_release_age {
} else {
let builder = ToolsetBuilder::new().with_resolve_options(lock_resolve_options.clone());
ts_owned = builder.build(&config).await?;
&ts_owned
} else {
config.get_toolset().await?
};

let scoped_config_paths = self.config_paths_in_lock_scope(&config, effective_config_files);
Expand Down Expand Up @@ -931,7 +924,12 @@ impl Lock {
}
// Skip unresolved symbolic versions (e.g., a lockfile poisoned with "latest"
// as the version). Pass 2's fallback will resolve these to a concrete version.
if tv.version == "latest" {
if tv.version == "latest"
|| backend
.ba()
.backend()
.is_ok_and(|backend| backend.is_rolling_channel(&tv.version))
{
continue;
}
push_unique_lock_tool(&mut all_tools, (backend.ba().as_ref().clone(), tv));
Expand All @@ -958,6 +956,9 @@ impl Lock {
for tv in &resolved_tv.versions {
if request_matches(&tv.request, request)
&& tv.version != "latest"
&& !ba.backend().is_ok_and(|backend| {
backend.is_rolling_channel(&tv.version)
})
{
matched_resolved = true;
push_unique_lock_tool(
Expand Down Expand Up @@ -1060,23 +1061,35 @@ impl Lock {
{
if let Some(Some(request)) = specified_versions.get(&ba.short) {
let version = request.version();
let backend = crate::backend::get(&ba);
let effective_version = match &backend {
Some(backend) => config.resolve_alias(backend, &version).await?,
None => version.clone(),
};
let request = ToolRequest::new_opts(
Arc::new(ba.clone()),
&version,
&effective_version,
tv.request.options(),
ToolSource::Argument,
);
let resolve_options = request
.as_ref()
.ok()
.and_then(|request| request.resolve_options(context.resolve_options).ok());
let is_rolling = backend
.is_some_and(|backend| backend.is_rolling_channel(&effective_version));
if let (Ok(request), Some(mut resolve_options)) = (request, resolve_options)
&& (self.bump || resolve_options.before_date.is_some())
&& (self.bump || resolve_options.before_date.is_some() || is_rolling)
{
resolve_options.use_locked_version = false;
resolve_options.latest_versions = true;
match request.resolve(config, &resolve_options).await {
Ok(resolved_tv) => tv = resolved_tv,
Err(err) if is_rolling => {
return Err(err.wrap_err(format!(
"failed to resolve specified rolling channel {request}"
)));
}
Err(err) => debug!("failed to resolve specified {request}: {err}"),
}
} else if version == "latest" {
Expand Down
2 changes: 2 additions & 0 deletions src/cli/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ impl Upgrade {
let opts = ResolveOptions {
use_locked_version: false,
latest_versions: true,
resolve_rolling_channels: false,
prefer_exact_version: false,
before_date,
before_date_from_default: false,
Expand Down Expand Up @@ -364,6 +365,7 @@ impl Upgrade {
resolve_options: ResolveOptions {
use_locked_version: false,
latest_versions: true,
resolve_rolling_channels: false,
prefer_exact_version: false,
before_date,
before_date_from_default: false,
Expand Down
1 change: 1 addition & 0 deletions src/cli/use.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ impl Use {
let mut resolve_options = ResolveOptions {
latest_versions: false,
use_locked_version: true,
resolve_rolling_channels: false,
prefer_exact_version: pin,
before_date: self.get_before_date()?,
before_date_from_default: false,
Expand Down
Loading
Loading