summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjimvdl <jimvdlind@gmail.com>2023-08-26 20:44:58 +0200
committerGitHub <noreply@github.com>2023-08-26 20:44:58 +0200
commit7bdf1f57b09ea605045254013a8200373451baf0 (patch)
tree0d7264dc2ca3aaf41b31e5226d8562cb4f68121e
parent879182059020e4669efae2296e98cdc8360a3bc2 (diff)
Let the CLI `typst update` itself without a package manager (#1887)
-rw-r--r--.github/workflows/release.yml4
-rw-r--r--Cargo.lock335
-rw-r--r--crates/typst-cli/Cargo.toml11
-rw-r--r--crates/typst-cli/build.rs2
-rw-r--r--crates/typst-cli/src/args.rs21
-rw-r--r--crates/typst-cli/src/main.rs17
-rw-r--r--crates/typst-cli/src/update.rs246
7 files changed, 619 insertions, 17 deletions
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 52de89ec..1e7156e4 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 56c9a056..80a99eee 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9,6 +9,17 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -118,6 +129,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
]
@@ -189,6 +206,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -207,10 +233,34 @@ 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"
@@ -249,6 +299,16 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -359,12 +419,27 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -423,6 +498,16 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -463,6 +548,17 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -600,6 +696,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -704,6 +806,16 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -790,6 +902,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1070,6 +1191,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1112,7 +1242,7 @@ checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
dependencies = [
"hermit-abi",
"io-lifetimes",
- "rustix",
+ "rustix 0.37.19",
"windows-sys 0.48.0",
]
@@ -1151,6 +1281,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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"
@@ -1261,6 +1400,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1296,6 +1441,17 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1518,6 +1674,17 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1536,6 +1703,18 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1612,6 +1791,12 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
]
@@ -2011,6 +2209,16 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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",
@@ -2082,6 +2290,28 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2210,6 +2440,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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]]
@@ -2573,11 +2809,17 @@ 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",
]
@@ -3353,6 +3601,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3441,6 +3698,26 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3453,6 +3730,36 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml
index c5b38be6..8396e97b 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 86325e1d..bd6a563d 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 2696a031..c741ecfc 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<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 {
diff --git a/crates/typst-cli/src/main.rs b/crates/typst-cli/src/main.rs
index 62f14566..b88a0ce4 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 00000000..617da4d1
--- /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 < &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"))
+}