Skip to content

Commit e296ddb

Browse files
committed
Add link and self-update
1 parent 963404d commit e296ddb

File tree

10 files changed

+148
-42
lines changed

10 files changed

+148
-42
lines changed

Cargo.lock

-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/paths/Cargo.toml

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ anyhow.workspace = true
1111
chrono = { workspace = true, features = ["now"] }
1212
fs2.workspace = true
1313
itoa.workspace = true
14-
semver.workspace = true
1514
serde.workspace = true
1615
thiserror.workspace = true
1716

crates/paths/src/cli.rs

+43-31
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::path::Path;
2+
13
use anyhow::Context;
24

35
use crate::utils::{path_type, PathBufExt};
@@ -30,53 +32,37 @@ path_type!(BinFile: file);
3032
path_type!(BinDir: dir);
3133

3234
impl BinDir {
33-
pub fn version_dir(&self, version: &semver::Version) -> VersionBinDir {
34-
VersionBinDir(self.0.join(version.to_string()))
35+
pub fn version_dir(&self, version: &str) -> VersionBinDir {
36+
VersionBinDir(self.0.join(version))
3537
}
3638

39+
pub const CURRENT_VERSION_DIR_NAME: &str = "current";
3740
pub fn current_version_dir(&self) -> VersionBinDir {
38-
VersionBinDir(self.0.join("current"))
41+
VersionBinDir(self.0.join(Self::CURRENT_VERSION_DIR_NAME))
3942
}
4043

41-
pub fn set_current_version(&self, version: &semver::Version) -> anyhow::Result<()> {
42-
let link_path = self.current_version_dir();
43-
#[cfg(unix)]
44-
{
45-
// remove the link if it already exists
46-
std::fs::remove_file(&link_path).ok();
47-
std::os::unix::fs::symlink(version.to_string(), link_path)?;
48-
}
49-
#[cfg(windows)]
50-
{
51-
junction::delete(&link_path).ok();
52-
let version_path = self.version_dir(version);
53-
// We won't be able to create a junction if the fs isn't NTFS, so fall back to trying
54-
// to make a symlink.
55-
junction::create(&version_path, &link_path)
56-
.or_else(|err| std::os::windows::fs::symlink_dir(version.to_string(), &link_path).or(Err(err)))?;
57-
}
58-
Ok(())
44+
pub fn set_current_version(&self, version: &str) -> anyhow::Result<()> {
45+
self.current_version_dir().link_to(self.version_dir(version).as_ref())
5946
}
6047

61-
pub fn current_version(&self) -> anyhow::Result<Option<semver::Version>> {
48+
pub fn current_version(&self) -> anyhow::Result<Option<String>> {
6249
match std::fs::read_link(self.current_version_dir()) {
63-
Ok(path) => path
64-
.to_str()
65-
.context("not utf8")
66-
.and_then(|s| s.parse::<semver::Version>().map_err(Into::into))
67-
.context("could not parse `current` symlink as a version number")
68-
.map(Some),
50+
Ok(path) => path.into_os_string().into_string().ok().context("not utf8").map(Some),
6951
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
7052
Err(e) => Err(e.into()),
7153
}
7254
}
7355

74-
pub fn installed_versions(&self) -> anyhow::Result<Vec<semver::Version>> {
56+
pub fn installed_versions(&self) -> anyhow::Result<Vec<String>> {
7557
self.read_dir()?
7658
.filter_map(|r| match r {
7759
Ok(entry) => {
78-
let parsed: semver::Version = entry.file_name().to_str()?.parse().ok()?;
79-
Some(anyhow::Ok(parsed))
60+
let name = entry.file_name();
61+
if name == Self::CURRENT_VERSION_DIR_NAME {
62+
None
63+
} else {
64+
entry.file_name().into_string().ok().map(Ok)
65+
}
8066
}
8167
Err(e) => Some(Err(e.into())),
8268
})
@@ -90,6 +76,32 @@ impl VersionBinDir {
9076
pub fn spacetimedb_cli(self) -> SpacetimedbCliBin {
9177
SpacetimedbCliBin(self.0.joined("spacetimedb-cli").with_exe_ext())
9278
}
79+
80+
pub fn create_custom(&self, path: &Path) -> anyhow::Result<()> {
81+
if std::fs::symlink_metadata(self).is_ok_and(|m| m.file_type().is_dir()) {
82+
anyhow::bail!("version already exists");
83+
}
84+
self.link_to(path)
85+
}
86+
87+
fn link_to(&self, path: &Path) -> anyhow::Result<()> {
88+
let rel_path = path.strip_prefix(self).unwrap_or(path);
89+
#[cfg(unix)]
90+
{
91+
// remove the link if it already exists
92+
std::fs::remove_file(self).ok();
93+
std::os::unix::fs::symlink(rel_path, self)?;
94+
}
95+
#[cfg(windows)]
96+
{
97+
junction::delete(self).ok();
98+
// We won't be able to create a junction if the fs isn't NTFS, so fall back to trying
99+
// to make a symlink.
100+
junction::create(path, self)
101+
.or_else(|err| std::os::windows::fs::symlink_dir(rel_path, self).or(Err(err)))?;
102+
}
103+
Ok(())
104+
}
93105
}
94106

95107
path_type!(SpacetimedbCliBin: file);

crates/update/src/cli.rs

+30
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ use std::ffi::OsString;
44
use std::future::Future;
55
use std::process::ExitCode;
66

7+
use anyhow::Context;
78
use spacetimedb_paths::{RootDir, SpacetimePaths};
89

910
mod install;
11+
mod link;
1012
mod list;
1113
mod uninstall;
1214
mod upgrade;
@@ -36,6 +38,26 @@ impl Args {
3638
crate::proxy::run_cli(Some(&paths), None, cli_args)
3739
}
3840
Subcommand::Version(version) => version.exec(&paths).map(|()| ExitCode::SUCCESS),
41+
Subcommand::SelfInstall { install_latest } => {
42+
let current_exe = std::env::current_exe().context("could not get current exe")?;
43+
let suppress_eexists = |r: std::io::Result<()>| {
44+
r.or_else(|e| (e.kind() == std::io::ErrorKind::AlreadyExists).then_some(()).ok_or(e))
45+
};
46+
suppress_eexists(paths.cli_bin_dir.create()).context("could not create bin dir")?;
47+
suppress_eexists(paths.cli_config_dir.create()).context("could not create config dir")?;
48+
suppress_eexists(paths.data_dir.create()).context("could not create data dir")?;
49+
paths
50+
.cli_bin_file
51+
.create_parent()
52+
.and_then(|()| std::fs::copy(&current_exe, &paths.cli_bin_file))
53+
.context("could not install binary")?;
54+
55+
if install_latest {
56+
upgrade::Upgrade {}.exec(&paths)?;
57+
}
58+
59+
Ok(ExitCode::SUCCESS)
60+
}
3961
}
4062
}
4163
}
@@ -48,6 +70,11 @@ enum Subcommand {
4870
#[clap(allow_hyphen_values = true)]
4971
args: Vec<OsString>,
5072
},
73+
SelfInstall {
74+
/// Download and install the latest CLI version after self-installing.
75+
#[arg(long)]
76+
install_latest: bool,
77+
},
5178
}
5279

5380
#[derive(clap::Args)]
@@ -66,6 +93,7 @@ impl Version {
6693
Upgrade(subcmd) => subcmd.exec(paths),
6794
Install(subcmd) => subcmd.exec(paths),
6895
Uninstall(subcmd) => subcmd.exec(paths),
96+
Link(subcmd) => subcmd.exec(paths),
6997
}
7098
}
7199
}
@@ -77,6 +105,8 @@ enum VersionSubcommand {
77105
Upgrade(upgrade::Upgrade),
78106
Install(install::Install),
79107
Uninstall(uninstall::Uninstall),
108+
#[command(hide = true)]
109+
Link(link::Link),
80110
}
81111

82112
fn reqwest_client() -> anyhow::Result<reqwest::Client> {

crates/update/src/cli/install.rs

+7-4
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ impl Install {
4141
let client = super::reqwest_client()?;
4242
let version = download_and_install(&client, Some(self.version), self.artifact_name, paths).await?;
4343
if self.r#use {
44-
paths.cli_bin_dir.set_current_version(&version)?;
44+
paths.cli_bin_dir.set_current_version(&version.to_string())?;
4545
}
4646
Ok(())
4747
})?
@@ -109,7 +109,7 @@ pub(super) async fn download_and_install(
109109
pb.set_style(pb_style);
110110
pb.set_message("unpacking...");
111111

112-
let version_dir = paths.cli_bin_dir.version_dir(&release_version);
112+
let version_dir = paths.cli_bin_dir.version_dir(&release_version.to_string());
113113
match artifact_type {
114114
ArtifactType::TarGz => {
115115
let tgz = archive.aggregate().reader();
@@ -144,11 +144,14 @@ impl ArtifactType {
144144
}
145145
}
146146

147-
pub(super) async fn available_releases(client: &reqwest::Client) -> anyhow::Result<Vec<semver::Version>> {
147+
pub(super) async fn available_releases(client: &reqwest::Client) -> anyhow::Result<Vec<String>> {
148148
let url = "https://api.github.com/repos/clockworklabs/SpacetimeDB/releases";
149149
let releases: Vec<Release> = client.get(url).send().await?.json().await?;
150150

151-
releases.into_iter().map(|release| release.version()).collect()
151+
releases
152+
.into_iter()
153+
.map(|release| Ok(release.version()?.to_string()))
154+
.collect()
152155
}
153156

154157
#[derive(Deserialize)]

crates/update/src/cli/link.rs

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use std::path::PathBuf;
2+
3+
use anyhow::Context;
4+
use spacetimedb_paths::cli::BinDir;
5+
use spacetimedb_paths::SpacetimePaths;
6+
7+
/// Set a local installation of SpacetimeDB as a custom version.
8+
#[derive(clap::Args)]
9+
pub(super) struct Link {
10+
/// The name of the custom installation, e.g. `dev`.
11+
name: String,
12+
/// The path to the directory with the SpacetimeDB binaries.
13+
path: PathBuf,
14+
15+
/// Switch to this version after it's created.
16+
#[arg(long)]
17+
r#use: bool,
18+
}
19+
20+
impl Link {
21+
pub(super) fn exec(self, paths: &SpacetimePaths) -> anyhow::Result<()> {
22+
anyhow::ensure!(
23+
self.name != BinDir::CURRENT_VERSION_DIR_NAME,
24+
"name cannot be `current`"
25+
);
26+
let mut path = std::env::current_dir()?;
27+
path.push(self.path);
28+
paths
29+
.cli_bin_dir
30+
.version_dir(&self.name)
31+
.create_custom(&path)
32+
.context("could not link custom version")?;
33+
if self.r#use {
34+
paths.cli_bin_dir.set_current_version(&self.name)?;
35+
}
36+
Ok(())
37+
}
38+
}

crates/update/src/cli/uninstall.rs

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
1+
use anyhow::Context;
2+
use spacetimedb_paths::cli::BinDir;
13
use spacetimedb_paths::SpacetimePaths;
24

35
use super::ForceYes;
46

57
/// Uninstall an installed SpacetimeDB version.
68
#[derive(clap::Args)]
79
pub(super) struct Uninstall {
8-
version: semver::Version,
10+
version: String,
911
#[command(flatten)]
1012
yes: ForceYes,
1113
}
1214

1315
impl Uninstall {
1416
pub(super) fn exec(self, paths: &SpacetimePaths) -> anyhow::Result<()> {
1517
let Self { version, yes } = self;
18+
anyhow::ensure!(
19+
version != BinDir::CURRENT_VERSION_DIR_NAME,
20+
"cannot remove `current` version"
21+
);
22+
match paths
23+
.cli_bin_dir
24+
.current_version()
25+
.context("couldn't read current version")
26+
{
27+
Ok(Some(current)) => anyhow::ensure!(version != current, "cannot uninstall currently used version"),
28+
Ok(None) => {}
29+
Err(e) => tracing::warn!("{e:#}"),
30+
}
1631
if yes.confirm(format!("Uninstall v{version}?"))? {
1732
let dir = paths.cli_bin_dir.version_dir(&version);
1833
std::fs::remove_dir_all(dir)?;

crates/update/src/cli/upgrade.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ impl Upgrade {
99
super::tokio_block_on(async {
1010
let client = super::reqwest_client()?;
1111
let version = super::install::download_and_install(&client, None, None, paths).await?;
12-
paths.cli_bin_dir.set_current_version(&version)?;
12+
paths.cli_bin_dir.set_current_version(&version.to_string())?;
1313

1414
let cur_version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
1515
if version > cur_version {
16-
let mut new_update_binary = paths.cli_bin_dir.version_dir(&version).0.join("spacetimedb-update");
16+
let mut new_update_binary = paths
17+
.cli_bin_dir
18+
.version_dir(&version.to_string())
19+
.0
20+
.join("spacetimedb-update");
1721
new_update_binary.set_extension(std::env::consts::EXE_EXTENSION);
1822
if new_update_binary.exists() {
1923
tokio::fs::copy(new_update_binary, &paths.cli_bin_file).await?;

crates/update/src/cli/use.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ pub(super) struct Use {
88

99
impl Use {
1010
pub(super) fn exec(self, paths: &SpacetimePaths) -> anyhow::Result<()> {
11-
paths.cli_bin_dir.set_current_version(&self.version)
11+
paths.cli_bin_dir.set_current_version(&self.version.to_string())
1212
}
1313
}

crates/update/src/main.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ fn main() -> anyhow::Result<ExitCode> {
1919
if cmd == "spacetimedb-update" {
2020
spacetimedb_update_main()
2121
} else if cmd == "spacetime" {
22-
proxy::run_cli(None, Some(argv0.as_os_str()), args.collect())
22+
let args = args.collect::<Vec<_>>();
23+
if args.first().is_some_and(|s| s == "version") {
24+
// if the first arg is unambiguously `version`, go straight to `spacetime version`
25+
spacetimedb_update_main()
26+
} else {
27+
proxy::run_cli(None, Some(argv0.as_os_str()), args)
28+
}
2329
} else {
2430
anyhow::bail!(
2531
"unknown command name for spacetimedb-update multicall binary: {}",

0 commit comments

Comments
 (0)