From 7bdf1f57b09ea605045254013a8200373451baf0 Mon Sep 17 00:00:00 2001 From: jimvdl Date: Sat, 26 Aug 2023 20:44:58 +0200 Subject: [PATCH] Let the CLI `typst update` itself without a package manager (#1887) --- .github/workflows/release.yml | 4 +- Cargo.lock | 335 +++++++++++++++++++++++++++++++-- crates/typst-cli/Cargo.toml | 11 +- crates/typst-cli/build.rs | 2 + crates/typst-cli/src/args.rs | 21 +++ crates/typst-cli/src/main.rs | 17 ++ crates/typst-cli/src/update.rs | 246 ++++++++++++++++++++++++ 7 files changed, 619 insertions(+), 17 deletions(-) create mode 100644 crates/typst-cli/src/update.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52de89ec3..1e7156e4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 56c9a056a..80a99eeec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml index c5b38be63..8396e97b7 100644 --- a/crates/typst-cli/Cargo.toml +++ b/crates/typst-cli/Cargo.toml @@ -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"] diff --git a/crates/typst-cli/build.rs b/crates/typst-cli/build.rs index 86325e1d1..bd6a563db 100644 --- a/crates/typst-cli/build.rs +++ b/crates/typst-cli/build.rs @@ -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"); diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index 2696a0319..c741ecfc0 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -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, + + /// 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 { diff --git a/crates/typst-cli/src/main.rs b/crates/typst-cli/src/main.rs index 62f145669..b88a0ce4d 100644 --- a/crates/typst-cli/src/main.rs +++ b/crates/typst-cli/src/main.rs @@ -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" + ) + } +} diff --git a/crates/typst-cli/src/update.rs b/crates/typst-cli/src/update.rs new file mode 100644 index 000000000..617da4d15 --- /dev/null +++ b/crates/typst-cli/src/update.rs @@ -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 < ¤t_tag { + bail!( + "downgrading requires the --force flag: \ + `typst update --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, +} + +impl Release { + /// Download the target release, or latest if version is `None`, from the + /// Typst repository. + pub fn from_tag(tag: Option) -> StrResult { + 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> { + 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> { + 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> { + 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 { + 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 { + #[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")) +}