Let the CLI typst update itself without a package manager (#1887)

This commit is contained in:
jimvdl 2023-08-26 20:44:58 +02:00 committed by GitHub
parent 8791820590
commit 7bdf1f57b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 619 additions and 17 deletions

View File

@ -45,11 +45,11 @@ jobs:
if: ${{ matrix.cross}}
run: |
cargo install cross --git https://github.com/cross-rs/cross.git
cross build -p typst-cli --release --target ${{ matrix.target }}
cross build -p typst-cli --release --target ${{ matrix.target }} --features self-update
- name: Run Cargo
if: ${{ !matrix.cross }}
run: cargo build -p typst-cli --release --target ${{ matrix.target }}
run: cargo build -p typst-cli --release --target ${{ matrix.target }} --features self-update
- name: create artifact directory
shell: bash

335
Cargo.lock generated
View File

@ -8,6 +8,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aes"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "ahash"
version = "0.8.3"
@ -117,6 +128,12 @@ version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "biblatex"
version = "0.8.0"
@ -169,9 +186,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.3.1"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84"
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
dependencies = [
"serde",
]
@ -188,6 +205,15 @@ dependencies = [
"wyz",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.13.0"
@ -206,11 +232,35 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bzip2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "cc"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
dependencies = [
"jobserver",
]
[[package]]
name = "cfg-if"
@ -248,6 +298,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "clap"
version = "4.2.7"
@ -358,12 +418,27 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cpufeatures"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
@ -422,6 +497,16 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "csv"
version = "1.2.1"
@ -462,6 +547,17 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "dirs"
version = "5.0.1"
@ -599,6 +695,12 @@ dependencies = [
"instant",
]
[[package]]
name = "fastrand"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
[[package]]
name = "fdeflate"
version = "0.3.0"
@ -703,6 +805,16 @@ version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getopts"
version = "0.2.21"
@ -789,6 +901,15 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "hypher"
version = "0.1.2"
@ -1069,6 +1190,15 @@ dependencies = [
"libc",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.12"
@ -1112,7 +1242,7 @@ checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
dependencies = [
"hermit-abi",
"io-lifetimes",
"rustix",
"rustix 0.37.19",
"windows-sys 0.48.0",
]
@ -1150,6 +1280,15 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "jobserver"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
dependencies = [
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.3.0"
@ -1211,9 +1350,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.144"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libdeflate-sys"
@ -1260,6 +1399,12 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f"
[[package]]
name = "linux-raw-sys"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
[[package]]
name = "lipsum"
version = "0.9.0"
@ -1295,6 +1440,17 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "lzma-sys"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "memchr"
version = "2.5.0"
@ -1517,6 +1673,17 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "password-hash"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]]
name = "paste"
version = "1.0.12"
@ -1535,6 +1702,18 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pbkdf2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
dependencies = [
"digest",
"hmac",
"password-hash",
"sha2",
]
[[package]]
name = "pdf-writer"
version = "0.8.0"
@ -1611,6 +1790,12 @@ dependencies = [
"ttf-parser",
]
[[package]]
name = "pkg-config"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "plist"
version = "1.4.3"
@ -1925,7 +2110,20 @@ dependencies = [
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"linux-raw-sys 0.3.7",
"windows-sys 0.48.0",
]
[[package]]
name = "rustix"
version = "0.38.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f"
dependencies = [
"bitflags 2.4.0",
"errno",
"libc",
"linux-raw-sys 0.4.5",
"windows-sys 0.48.0",
]
@ -2010,6 +2208,16 @@ dependencies = [
"untrusted",
]
[[package]]
name = "self-replace"
version = "1.3.5"
source = "git+https://github.com/typst/self-replace#2e6d5e4808bba73b713fd85cf5616b7d846143c2"
dependencies = [
"fastrand 1.9.0",
"tempfile",
"windows-sys 0.48.0",
]
[[package]]
name = "semver"
version = "1.0.17"
@ -2038,9 +2246,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.96"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
dependencies = [
"itoa",
"ryu",
@ -2081,6 +2289,28 @@ dependencies = [
"unsafe-libyaml",
]
[[package]]
name = "sha1"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"
@ -2209,6 +2439,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09eab8a83bff89ba2200bd4c59be45c7c787f988431b936099a5a266c957f2f9"
[[package]]
name = "subtle"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "svg2pdf"
version = "0.6.0"
@ -2307,15 +2543,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.5.0"
version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998"
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
dependencies = [
"cfg-if",
"fastrand",
"fastrand 2.0.0",
"redox_syscall 0.3.5",
"rustix",
"windows-sys 0.45.0",
"rustix 0.38.8",
"windows-sys 0.48.0",
]
[[package]]
@ -2572,12 +2808,18 @@ version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typenum"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "typst"
version = "0.7.0"
dependencies = [
"base64",
"bitflags 2.3.1",
"bitflags 2.4.0",
"bytemuck",
"comemo",
"ecow",
@ -2639,6 +2881,8 @@ dependencies = [
"open",
"pathdiff 0.1.0",
"same-file",
"self-replace",
"semver",
"serde",
"serde_json",
"serde_yaml 0.9.25",
@ -2653,6 +2897,8 @@ dependencies = [
"typst-library",
"ureq",
"walkdir",
"xz2",
"zip",
]
[[package]]
@ -2894,6 +3140,8 @@ dependencies = [
"once_cell",
"rustls",
"rustls-webpki",
"serde",
"serde_json",
"url",
"webpki-roots",
]
@ -3352,6 +3600,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fd742bbbb930fc972b28bf66b7546dfbc7bb9a4c7924299df0ae6a5641fcadf"
[[package]]
name = "xz2"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
dependencies = [
"lzma-sys",
]
[[package]]
name = "yaml-front-matter"
version = "0.1.0"
@ -3440,6 +3697,26 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zip"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"aes",
"byteorder",
"bzip2",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"flate2",
"hmac",
"pbkdf2",
"sha1",
"time",
"zstd",
]
[[package]]
name = "zopfli"
version = "0.7.4"
@ -3452,6 +3729,36 @@ dependencies = [
"typed-arena",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "5.0.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
dependencies = [
"libc",
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.8+zstd.1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "zune-inflate"
version = "0.2.54"

View File

@ -35,20 +35,26 @@ once_cell = "1"
open = "4.0.2"
pathdiff = "0.1"
same-file = "1"
# https://github.com/mitsuhiko/self-replace/pull/16
self-replace = { git = "https://github.com/typst/self-replace", optional = true }
semver = "1"
serde = "1.0.184"
serde_json = "1"
serde_yaml = "0.9"
siphasher = "0.3"
tar = "0.4"
tempfile = "3.5.0"
tempfile = "3.7.0"
tracing = "0.1.37"
tracing-error = "0.2"
tracing-flame = "0.2.0"
tracing-subscriber = "0.3.17"
ureq = "2"
walkdir = "2"
xz2 = { version = "0.1", optional = true }
zip = { version = "0.6", optional = true }
[build-dependencies]
semver = "1"
clap = { version = "4.2.4", features = ["derive", "string"] }
clap_complete = "4.2.1"
clap_mangen = "0.2.10"
@ -61,3 +67,6 @@ default = ["embed-fonts"]
# - For math: New Computer Modern Math
# - For code: Deja Vu Sans Mono
embed-fonts = []
# Permits the CLI to update itself without a package manager
self-update = ["dep:self-replace", "dep:xz2", "dep:zip", "ureq/json"]

View File

@ -12,6 +12,8 @@ use clap_mangen::Man;
mod args;
fn main() {
// https://stackoverflow.com/a/51311222/11494565
println!("cargo:rustc-env=TARGET={}", env::var("TARGET").unwrap());
println!("cargo:rerun-if-env-changed=TYPST_VERSION");
println!("cargo:rerun-if-env-changed=GEN_ARTIFACTS");

View File

@ -1,6 +1,8 @@
use std::fmt::{self, Display, Formatter};
use std::path::PathBuf;
use semver::Version;
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
/// The Typst compiler.
@ -34,6 +36,10 @@ pub enum Command {
/// Lists all discovered fonts in system and custom font paths
Fonts(FontsCommand),
/// Self update the Typst CLI
#[cfg_attr(not(feature = "self-update"), doc = " (disabled)")]
Update(UpdateCommand),
}
/// Compiles an input file into a supported output format
@ -154,6 +160,21 @@ impl Display for DiagnosticFormat {
}
}
#[derive(Debug, Clone, Parser)]
pub struct UpdateCommand {
/// Which version to update to (defaults to latest)
pub version: Option<Version>,
/// Forces a downgrade to an older version (required for downgrading)
#[clap(long, default_value_t = false)]
pub force: bool,
/// Reverts to the version from before the last update (only possible if
/// `typst update` has previously ran)
#[clap(long, default_value_t = false, exclusive = true)]
pub revert: bool,
}
/// Which format to use for the generated output file.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
pub enum OutputFormat {

View File

@ -4,6 +4,8 @@ mod fonts;
mod package;
mod query;
mod tracing;
#[cfg(feature = "self-update")]
mod update;
mod watch;
mod world;
@ -39,6 +41,7 @@ fn main() -> ExitCode {
Command::Watch(command) => crate::watch::watch(command),
Command::Query(command) => crate::query::query(command),
Command::Fonts(command) => crate::fonts::fonts(command),
Command::Update(command) => crate::update::update(command),
};
if let Err(msg) = res {
@ -79,3 +82,17 @@ fn color_stream() -> termcolor::StandardStream {
fn typst_version() -> &'static str {
env!("TYPST_VERSION")
}
#[cfg(not(feature = "self-update"))]
mod update {
use crate::args::UpdateCommand;
use typst::diag::{bail, StrResult};
pub fn update(_: UpdateCommand) -> StrResult<()> {
bail!(
"self-updating is not enabled for this executable, \
please update with the package manager or mechanism \
used for initial installation"
)
}
}

View File

@ -0,0 +1,246 @@
use std::env;
use std::fs;
use std::io::{Cursor, Read, Write};
use std::path::PathBuf;
use semver::Version;
use serde::Deserialize;
use tempfile::NamedTempFile;
use typst::{diag::bail, diag::StrResult, eval::eco_format};
use xz2::bufread::XzDecoder;
use zip::ZipArchive;
use crate::args::UpdateCommand;
const TYPST_GITHUB_ORG: &str = "typst";
const TYPST_REPO: &str = "typst";
/// Self update the Typst CLI binary.
///
/// Fetches a target release or the latest release (if no version was specified)
/// from GitHub, unpacks it and self replaces the current binary with the
/// pre-compiled asset from the downloaded release.
pub fn update(command: UpdateCommand) -> StrResult<()> {
if let Some(ref version) = command.version {
let current_tag = env!("CARGO_PKG_VERSION").parse().unwrap();
if version < &Version::new(0, 8, 0) {
eprintln!(
"Note: Versions older than 0.8.0 will not have \
the update command available."
);
}
if !command.force && version < &current_tag {
bail!(
"downgrading requires the --force flag: \
`typst update <VERSION> --force`"
);
}
}
let backup_path = backup_path()?;
if command.revert {
if !backup_path.exists() {
bail!(
"unable to revert, no backup found (searched at {})",
backup_path.display()
);
}
return self_replace::self_replace(&backup_path)
.and_then(|_| fs::remove_file(&backup_path))
.map_err(|err| eco_format!("failed to revert to backup: {err}"));
}
let current_exe = env::current_exe().map_err(|err| {
eco_format!("failed to locate path of the running executable: {err}")
})?;
fs::copy(current_exe, &backup_path)
.map_err(|err| eco_format!("failed to create backup: {err}"))?;
let release = Release::from_tag(command.version)?;
if !update_needed(&release)? && !command.force {
eprintln!("Already up-to-date.");
return Ok(());
}
let binary_data = release.download_binary(needed_asset()?)?;
let mut temp_exe = NamedTempFile::new()
.map_err(|err| eco_format!("failed to create temporary file: {err}"))?;
temp_exe
.write_all(&binary_data)
.map_err(|err| eco_format!("failed to write binary data: {err}"))?;
self_replace::self_replace(&temp_exe).map_err(|err| {
fs::remove_file(&temp_exe).ok();
eco_format!("failed to self-replace running executable: {err}")
})
}
/// Assets belonging to a GitHub release.
///
/// Primarily used to download pre-compiled Typst CLI binaries.
#[derive(Debug, Deserialize)]
struct Asset {
name: String,
browser_download_url: String,
}
/// A GitHub release.
#[derive(Debug, Deserialize)]
struct Release {
tag_name: String,
assets: Vec<Asset>,
}
impl Release {
/// Download the target release, or latest if version is `None`, from the
/// Typst repository.
pub fn from_tag(tag: Option<Version>) -> StrResult<Release> {
let url = match tag {
Some(tag) => format!(
"https://api.github.com/repos/{}/{}/releases/tags/v{}",
TYPST_GITHUB_ORG, TYPST_REPO, tag
),
None => format!(
"https://api.github.com/repos/{}/{}/releases/latest",
TYPST_GITHUB_ORG, TYPST_REPO
),
};
match ureq::get(&url).call() {
Ok(response) => response
.into_json()
.map_err(|err| eco_format!("unable to parse JSON response: {err}")),
Err(ureq::Error::Status(404, _)) => {
bail!("release not found (searched at {url})")
}
Err(_) => bail!("failed to download release (network failed)"),
}
}
/// Download the binary from a given [`Release`] and select the
/// corresponding asset for this target platform, returning the raw binary
/// data.
pub fn download_binary(&self, asset_name: &str) -> StrResult<Vec<u8>> {
let asset = self
.assets
.iter()
.find(|a| a.name.starts_with(asset_name))
.ok_or("could not find release for your target platform")?;
eprintln!("Downloading release ...");
let response = match ureq::get(&asset.browser_download_url).call() {
Ok(response) => response,
Err(ureq::Error::Status(404, _)) => {
bail!("asset not found (searched for {})", asset.name);
}
Err(_) => bail!("failed to load asset (network failed)"),
};
let mut data = Vec::new();
response
.into_reader()
.read_to_end(&mut data)
.map_err(|err| eco_format!("failed to read response buffer: {err}"))?;
if asset_name.contains("windows") {
extract_binary_from_zip(&data, asset_name)
} else {
extract_binary_from_tar_xz(&data)
}
}
}
/// Extract the Typst binary from a ZIP archive.
fn extract_binary_from_zip(data: &[u8], asset_name: &str) -> StrResult<Vec<u8>> {
let mut archive = ZipArchive::new(Cursor::new(data))
.map_err(|err| eco_format!("failed to extract ZIP archive: {err}"))?;
let mut file = archive
.by_name(&format!("{asset_name}/typst.exe"))
.map_err(|_| "ZIP archive did not contain Typst binary")?;
let mut buffer = vec![];
file.read_to_end(&mut buffer).map_err(|err| {
eco_format!("failed to read binary data from ZIP archive: {err}")
})?;
Ok(buffer)
}
/// Extract the Typst binary from a `.tar.xz` archive.
fn extract_binary_from_tar_xz(data: &[u8]) -> StrResult<Vec<u8>> {
let mut archive = tar::Archive::new(XzDecoder::new(Cursor::new(data)));
let mut file = archive
.entries()
.map_err(|err| eco_format!("failed to extract tar.xz archive: {err}"))?
.filter_map(Result::ok)
.find(|e| e.path().unwrap_or_default().ends_with("typst"))
.ok_or("tar.xz archive did not contain Typst binary")?;
let mut buffer = vec![];
file.read_to_end(&mut buffer).map_err(|err| {
eco_format!("failed to read binary data from tar.xz archive: {err}")
})?;
Ok(buffer)
}
/// Determine what asset to download according to the target platform the CLI
/// is running on.
fn needed_asset() -> StrResult<&'static str> {
Ok(match env!("TARGET") {
"x86_64-unknown-linux-gnu" => "typst-x86_64-unknown-linux-musl",
"x86_64-unknown-linux-musl" => "typst-x86_64-unknown-linux-musl",
"aarch64-unknown-linux-musl" => "typst-aarch64-unknown-linux-musl",
"aarch64-unknown-linux-gnu" => "typst-aarch64-unknown-linux-musl",
"armv7-unknown-linux-musleabi" => "typst-armv7-unknown-linux-musleabi",
"x86_64-apple-darwin" => "typst-x86_64-apple-darwin",
"aarch64-apple-darwin" => "typst-aarch64-apple-darwin",
"x86_64-pc-windows-msvc" => "typst-x86_64-pc-windows-msvc",
target => bail!("unsupported target: {target}"),
})
}
/// Compare the release version to the CLI version to see if an update is needed.
fn update_needed(release: &Release) -> StrResult<bool> {
let current_tag: Version = env!("CARGO_PKG_VERSION").parse().unwrap();
let new_tag: Version = release
.tag_name
.strip_prefix('v')
.unwrap_or(&release.tag_name)
.parse()
.map_err(|_| "release tag not in semver format")?;
Ok(new_tag > current_tag)
}
/// Path to a potential backup file.
///
/// The backup will be placed in one of the following directories, depending on
/// the platform:
/// - `$XDG_STATE_HOME` or `~/.local/state` on Linux
/// - `$XDG_DATA_HOME` or `~/.local/share` if the above path isn't available
/// - `~/Library/Application Support` on macOS
/// - `%APPDATA%` on Windows
fn backup_path() -> StrResult<PathBuf> {
#[cfg(target_os = "linux")]
let root_backup_dir = dirs::state_dir()
.or_else(|| dirs::data_dir())
.ok_or("unable to locate local data or state directory")?;
#[cfg(not(target_os = "linux"))]
let root_backup_dir =
dirs::data_dir().ok_or("unable to locate local data directory")?;
let backup_dir = root_backup_dir.join("typst");
fs::create_dir_all(&backup_dir)
.map_err(|err| eco_format!("failed to create backup directory: {err}"))?;
Ok(backup_dir.join("typst_backup.part"))
}