Repository: axodotdev/cargo-dist
Pull request #243 (2023-05-09 02:22:29)

add basic support for shasums 
We now default generate .sha256 checksum files (one for each archive). Example:

![image](https://user-images.githubusercontent.com/1136864/236977229-40c76750-a0ad-4ba7-8618-0975a710f086.png)

A new `checksum = "..."` config has been added, accepting 3 values:

* "sha256" (default)
* "sha512"
* "false" (disable checksumming)

All checksums are implemented with the sha2 crate and match sha256sum and sha512sum's hashes. The resulting file is just a lowercase hex string of the hash (no filename included, since each file gets its own .sha256 file). I've defaulted to sha256 since that seems to be the default other people use, and if you're really serious about hardening this stuff you probably want actual signing/pki backing this up (like #120).

I've also cleaned up the Github Release table output a bit as part of this.

Gankra: Oh I guess this drive-by fixes #200 by just not describing the artifact anymore

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 489f69be..a705d09b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+# Version 0.0.7 (2023-05-09)
+
+This is just a quick little release that makes the npm package tarballs we can generate
+look like "properly" packed tarballs that can be directly published to npm without unpacking them.
+This allows you to `npm publish URL_TO_TARBALL` directly without any issues.
+
+@gankra [impl](https://github.com/axodotdev/cargo-dist/pull/246)
+
+
+
+
 # Version 0.0.6 (2023-05-03)
 
 This release is a pretty big improvement to cargo-dist's UX!
diff --git a/Cargo.lock b/Cargo.lock
index abf2a8d4..06ea7042 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -120,6 +120,25 @@ dependencies = [
  "url",
 ]
 
+[[package]]
+name = "axoasset"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc3347278d8b9a15b998d45a0ff07919ca2b98795445aa550cd4222ddacd00e6"
+dependencies = [
+ "camino",
+ "flate2",
+ "image",
+ "miette",
+ "mime",
+ "reqwest",
+ "tar",
+ "thiserror",
+ "url",
+ "xz2",
+ "zip",
+]
+
 [[package]]
 name = "axocli"
 version = "0.1.0"
@@ -143,7 +162,7 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "db85ff14dd93eb45363fa28d270c5e599da0ead233bc90be773b1968b31547ec"
 dependencies = [
- "axoasset",
+ "axoasset 0.1.1",
  "camino",
  "clap",
  "console",
@@ -183,6 +202,12 @@ dependencies = [
  "backtrace",
 ]
 
+[[package]]
+name = "base64"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
+
 [[package]]
 name = "base64ct"
 version = "1.6.0"
@@ -266,8 +291,9 @@ dependencies = [
 
 [[package]]
 name = "cargo-dist"
-version = "0.0.6"
+version = "0.0.7"
 dependencies = [
+ "axoasset 0.2.0",
  "axocli",
  "axoproject",
  "camino",
@@ -286,6 +312,7 @@ dependencies = [
  "semver",
  "serde",
  "serde_json",
+ "sha2",
  "tar",
  "thiserror",
  "toml_edit",
@@ -296,7 +323,7 @@ dependencies = [
 
 [[package]]
 name = "cargo-dist-schema"
-version = "0.0.6"
+version = "0.0.7"
 dependencies = [
  "camino",
  "insta",
@@ -456,6 +483,22 @@ version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
 
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[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.6"
@@ -586,6 +629,15 @@ version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
 
+[[package]]
+name = "encoding_rs"
+version = "0.8.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
+dependencies = [
+ "cfg-if",
+]
+
 [[package]]
 name = "errno"
 version = "0.3.1"
@@ -682,6 +734,27 @@ dependencies = [
  "spin",
 ]
 
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
 [[package]]
 name = "form_urlencoded"
 version = "1.1.0"
@@ -691,6 +764,15 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "futures-channel"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
+dependencies = [
+ "futures-core",
+]
+
 [[package]]
 name = "futures-core"
 version = "0.3.28"
@@ -703,6 +785,24 @@ version = "0.3.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
 
+[[package]]
+name = "futures-task"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+
+[[package]]
+name = "futures-util"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+]
+
 [[package]]
 name = "generic-array"
 version = "0.14.7"
@@ -774,6 +874,25 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "92620684d99f750bae383ecb3be3748142d6095760afd5cbcf2261e9a279d780"
 
+[[package]]
+name = "h2"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
 [[package]]
 name = "half"
 version = "2.2.1"
@@ -822,6 +941,77 @@ dependencies = [
  "digest",
 ]
 
+[[package]]
+name = "http"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[package]]
+name = "hyper"
+version = "0.14.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
 [[package]]
 name = "idna"
 version = "0.3.0"
@@ -895,6 +1085,12 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "ipnet"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
+
 [[package]]
 name = "is-terminal"
 version = "0.4.7"
@@ -1087,6 +1283,18 @@ dependencies = [
  "simd-adler32",
 ]
 
+[[package]]
+name = "mio"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.45.0",
+]
+
 [[package]]
 name = "nanorand"
 version = "0.7.0"
@@ -1096,6 +1304,24 @@ dependencies = [
  "getrandom",
 ]
 
+[[package]]
+name = "native-tls"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
 [[package]]
 name = "nested"
 version = "0.1.1"
@@ -1182,6 +1408,50 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
+[[package]]
+name = "openssl"
+version = "0.10.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "overload"
 version = "0.1.1"
@@ -1280,6 +1550,12 @@ version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
 
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
 [[package]]
 name = "pkg-config"
 version = "0.3.26"
@@ -1389,6 +1665,43 @@ version = "0.6.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
 
+[[package]]
+name = "reqwest"
+version = "0.11.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
 [[package]]
 name = "rustc-demangle"
 version = "0.1.23"
@@ -1415,6 +1728,15 @@ version = "1.0.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
 
+[[package]]
+name = "schannel"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
+dependencies = [
+ "windows-sys 0.42.0",
+]
+
 [[package]]
 name = "schemars"
 version = "0.8.12"
@@ -1445,6 +1767,29 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
+[[package]]
+name = "security-framework"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "semver"
 version = "1.0.17"
@@ -1496,6 +1841,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
 [[package]]
 name = "sha1"
 version = "0.10.5"
@@ -1545,6 +1902,15 @@ version = "2.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf"
 
+[[package]]
+name = "slab"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "smallvec"
 version = "1.10.0"
@@ -1557,6 +1923,16 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
 
+[[package]]
+name = "socket2"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
 [[package]]
 name = "spin"
 version = "0.9.8"
@@ -1768,6 +2144,45 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
+[[package]]
+name = "tokio"
+version = "1.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
+]
+
 [[package]]
 name = "toml_datetime"
 version = "0.5.1"
@@ -1786,6 +2201,12 @@ dependencies = [
  "toml_datetime",
 ]
 
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
 [[package]]
 name = "tracing"
 version = "0.1.37"
@@ -1845,6 +2266,12 @@ dependencies = [
  "tracing-log",
 ]
 
+[[package]]
+name = "try-lock"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
+
 [[package]]
 name = "typenum"
 version = "1.16.0"
@@ -1917,12 +2344,28 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
 [[package]]
 name = "version_check"
 version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
 [[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
@@ -1954,6 +2397,18 @@ dependencies = [
  "wasm-bindgen-shared",
 ]
 
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
 [[package]]
 name = "wasm-bindgen-macro"
 version = "0.2.84"
@@ -1983,6 +2438,16 @@ version = "0.2.84"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
 
+[[package]]
+name = "web-sys"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "weezl"
 version = "0.1.7"
@@ -2158,6 +2623,15 @@ version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
 
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "xattr"
 version = "0.2.3"
diff --git a/book/src/config.md b/book/src/config.md
index 5955c68d..4154c62f 100644
--- a/book/src/config.md
+++ b/book/src/config.md
@@ -162,6 +162,21 @@ Specifies that [npm installers][] should be published under the given [scope][].
 If no scope is specified the package will be global.
 
 
+### checksum
+
+Example `checksum = "sha512"`
+
+Specifies how to checksum [executable-zips][]. Supported values:
+
+* "sha256" (default) - generate a .sha256 file for each archive
+* "sha512" - generate a .sha512 file for each archive
+* "false" - do not generate any checksums
+
+The hashes should match the result that sha256sum and sha512sum generate. The current format is just a file containing the hash of that file and nothing else.
+
+Future work is planned to [support more robust signed checksums][issue-sigstore].
+
+
 ## Subsetting CI Flags
 
 Several `metadata.dist` configs have globally available CLI equivalents. These can be used to select a subset of `metadata.dist` list for that run. If you don't pass any, it will be as-if you passed all the values in `metadata.dist`. You can pass these flags multiple times to provide a list. This includes:
@@ -191,4 +206,5 @@ Caveat: the default "host" Artifact Mode does something fuzzier with `--target`
 [installers]: ./installers.md
 [artifact-url]: ./installers.md#artifact-download-url
 [scope]: https://docs.npmjs.com/cli/v9/using-npm/scope
-[npm installers]: ./installers.md#npm
\ No newline at end of file
+[npm installers]: ./installers.md#npm
+[issue-sigstore]: https://github.com/axodotdev/cargo-dist/issues/120
\ No newline at end of file
diff --git a/book/src/installers.md b/book/src/installers.md
index f57add4a..b3c9e208 100644
--- a/book/src/installers.md
+++ b/book/src/installers.md
@@ -22,6 +22,8 @@ These keys can be specified via [`installer` in your cargo-dist config][installe
 
 ### shell
 
+> since 0.0.3
+
 This provides a shell script (installer.sh) which detects the current platform, fetches the best possible [executable-zip][] from your [artifact download URL][artifact-download-url], and copies the binary into your [cargo home][], where presumably it will end up on your PATH.
 
 This kind of installer is ideal for bootstrapping setup on a fairly bare-bones system.
@@ -52,6 +54,8 @@ In an ideal world all of these caveats improve (except for maybe relying on tar/
 
 ### powershell
 
+> since 0.0.3
+
 This provides a powershell script (installer.ps1) which detects the current platform, fetches the best possible [executable-zip][] from your [artifact download URL][artifact-download-url], and copies the binary into your [cargo home][], where presumably it will end up on your PATH.
 
 This kind of installer is ideal for bootstrapping setup on a fairly bare-bones system.
@@ -83,7 +87,9 @@ In an ideal world most of these caveats improve (except for maybe the requiremen
 
 ### npm
 
-This provides a tarball containing an npm package (npm-package.tar.gz) which when installed into an npm project: detects the current platform, fetches the best possible [executable-zip][] from your [artifact download URL][artifact-download-url], and copies the binary into your node_modules, where it can be run with things like `npx`.
+> since 0.0.6
+
+This provides a tarball containing an npm package (npm-package.tar.gz) which when installed into an npm project: detects the current platform, fetches the best possible [executable-zip][] from your [artifact download URL][artifact-download-url], and copies the binary into your node_modules. This can be used to install the binaries like any other npm package, or to run them with `npx`.
 
 This kind of installer is ideal for making a native Rust tool available to JS devs.
 
@@ -93,13 +99,27 @@ An "installer hint" will be provided that shows how to install via `npm` like so
 npm install @axodotdev/cargodisttest@0.2.0
 ```
 
-**cargo-dist does not publish the package for you, you need to do that manually once the tarball is built.** You can do this in one of 3 ways:
+**cargo-dist does not publish the package for you, you need to do that manually once the tarball is built.** Conveniently, npm supports publishing from a url-to-a-tarball directly, and since 0.0.7 we make our tarballs look like "proper" npm package tarballs, so you can just do this:
+
+```sh
+npm publish URL_TO_TARBALL
+```
+
+You can find the URL to the tarball at the bottom of the Github Release, inside the collapsible "assets" dropdown (*-npm-package.tar.gz). The format of the url is:
+
+```text
+<repo>/releases/download/<tag>/<app-name>-npm-package.tar.gz
+```
+
+Example:
+
+https://github.com/axodotdev/oranda/releases/download/v0.0.3/oranda-npm-package.tar.gz
+
+If you're cutting a stable release (not-prerelease), you can use the "latest" URL format:
 
-* remotely with `npm publish URL_TO_TARBALL` (yes npm genuinely supports this!)
-* locally after downloading with `npm publish PATH_TO_TARBALL`
-* locally after unpacking with `npm publish`
+https://github.com/axodotdev/oranda/releases/latest/download/oranda-npm-package.tar.gz
 
-(In testing we've seen some inconsistent results on Windows where the README isn't properly detected by npm's website when publishing remotely, but is when run on the unpacked results... No Idea What's Up With That, [let us know if you can reproduce/isolate the issue][issue-npm-readme].)
+In the future we may [introduce more streamlined CI-based publishing workflows][issue-npm-ci].
 
 [You can set the @scope the package is published under with the npm-scope cargo-dist config][npm-scope].
 
@@ -202,6 +222,7 @@ Although that's still missing things like [Windows crt-static workarounds][crt-s
 
 [issue-info-install]: https://github.com/axodotdev/cargo-dist/issues/72
 [issue-npm-readme]: https://github.com/axodotdev/cargo-dist/issues/238
+[issue-npm-install]: https://github.com/axodotdev/cargo-dist/issues/245
 [linux-pm-issue]: https://github.com/axodotdev/cargo-dist/issues/76
 [windows-pm-issue]: https://github.com/axodotdev/cargo-dist/issues/87
 [msi-installer-issue]: https://github.com/axodotdev/cargo-dist/issues/23
diff --git a/cargo-dist-schema/Cargo.toml b/cargo-dist-schema/Cargo.toml
index fc0df91f..57ccc564 100644
--- a/cargo-dist-schema/Cargo.toml
+++ b/cargo-dist-schema/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "cargo-dist-schema"
 description = "Schema information for cargo-dist's dist-manifest.json"
-version = "0.0.6"
+version = "0.0.7"
 edition = "2021"
 license = "MIT OR Apache-2.0"
 repository = "https://github.com/axodotdev/cargo-dist"
diff --git a/cargo-dist-schema/cargo-dist-json-schema.json b/cargo-dist-schema/cargo-dist-json-schema.json
index 84fa435a..88fcf59d 100644
--- a/cargo-dist-schema/cargo-dist-json-schema.json
+++ b/cargo-dist-schema/cargo-dist-json-schema.json
@@ -109,6 +109,21 @@
             }
           }
         },
+        {
+          "description": "A checksum of another artifact",
+          "type": "object",
+          "required": [
+            "kind"
+          ],
+          "properties": {
+            "kind": {
+              "type": "string",
+              "enum": [
+                "checksum"
+              ]
+            }
+          }
+        },
         {
           "description": "Unknown to this version of cargo-dist-schema\n\nThis is a fallback for forward/backward-compat",
           "type": "object",
@@ -133,6 +148,13 @@
             "$ref": "#/definitions/Asset"
           }
         },
+        "checksum": {
+          "description": "id of an that contains the checksum for this artifact",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
         "description": {
           "description": "A brief description of what this artifact is",
           "type": [
diff --git a/cargo-dist-schema/src/lib.rs b/cargo-dist-schema/src/lib.rs
index 0c100b4e..8801ad1f 100644
--- a/cargo-dist-schema/src/lib.rs
+++ b/cargo-dist-schema/src/lib.rs
@@ -112,6 +112,10 @@ pub struct Artifact {
     #[serde(skip_serializing_if = "Option::is_none")]
     #[serde(default)]
     pub description: Option<String>,
+    /// id of an that contains the checksum for this artifact
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(default)]
+    pub checksum: Option<String>,
 }
 
 /// An asset contained in an artifact (executable, license, etc.)
@@ -169,6 +173,9 @@ pub enum ArtifactKind {
     /// Installer
     #[serde(rename = "installer")]
     Installer,
+    /// A checksum of another artifact
+    #[serde(rename = "checksum")]
+    Checksum,
     /// Unknown to this version of cargo-dist-schema
     ///
     /// This is a fallback for forward/backward-compat
diff --git a/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap b/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
index f65772af..8b8fde0e 100644
--- a/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
+++ b/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
@@ -113,6 +113,21 @@ expression: json_schema
             }
           }
         },
+        {
+          "description": "A checksum of another artifact",
+          "type": "object",
+          "required": [
+            "kind"
+          ],
+          "properties": {
+            "kind": {
+              "type": "string",
+              "enum": [
+                "checksum"
+              ]
+            }
+          }
+        },
         {
           "description": "Unknown to this version of cargo-dist-schema\n\nThis is a fallback for forward/backward-compat",
           "type": "object",
@@ -137,6 +152,13 @@ expression: json_schema
             "$ref": "#/definitions/Asset"
           }
         },
+        "checksum": {
+          "description": "id of an that contains the checksum for this artifact",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
         "description": {
           "description": "A brief description of what this artifact is",
           "type": [
diff --git a/cargo-dist/Cargo.toml b/cargo-dist/Cargo.toml
index 8fe812c4..882b282b 100644
--- a/cargo-dist/Cargo.toml
+++ b/cargo-dist/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "cargo-dist"
 description = "Shippable application packaging for Rust"
-version = "0.0.6"
+version = "0.0.7"
 edition = "2021"
 license = "MIT OR Apache-2.0"
 repository = "https://github.com/axodotdev/cargo-dist"
@@ -30,7 +30,7 @@ clap-cargo = { version = "0.10.0", optional = true }
 axocli = { version = "0.1.0", optional = true }
 
 # Features used by the cli and library
-cargo-dist-schema = { version = "=0.0.6", path = "../cargo-dist-schema" }
+cargo-dist-schema = { version = "=0.0.7", path = "../cargo-dist-schema" }
 miette = { version = "5.6.0" }
 thiserror = "1.0.35"
 tracing = { version = "0.1.36", features = ["log"] }
@@ -48,6 +48,8 @@ parse-changelog = { version = "0.5.3", default-features = false }
 newline-converter = "0.2.2"
 axoproject = { version = "0.3.0", default-features = false, features = ["cargo-projects"] }
 dialoguer = "0.10.4"
+axoasset = "0.2.0"
+sha2 = "0.10.6"
 
 [dev-dependencies]
 insta = { version = "1.26.0", features = ["filters"] }
diff --git a/cargo-dist/oranda.json b/cargo-dist/oranda.json
index 8d0cd696..7e88916a 100644
--- a/cargo-dist/oranda.json
+++ b/cargo-dist/oranda.json
@@ -15,7 +15,6 @@
         "crates.io": "cargo install cargo-dist --locked --profile=dist"
     }
   },
-  "additional_css": ["oranda/cargo-dist.css"],
   "analytics": {
     "plausible": {
       "domain": "opensource.axo.dev"
diff --git a/cargo-dist/oranda/cargo-dist.css b/cargo-dist/oranda/cargo-dist.css
deleted file mode 100644
index 8d2180bc..00000000
--- a/cargo-dist/oranda/cargo-dist.css
+++ /dev/null
@@ -1,7 +0,0 @@
-header h1 {
-    padding-bottom: 0.2em;
-}
-
-.nav ul li {
-    text-transform: uppercase;
-}
diff --git a/cargo-dist/src/errors.rs b/cargo-dist/src/errors.rs
index 4aed5d1b..979b70fb 100644
--- a/cargo-dist/src/errors.rs
+++ b/cargo-dist/src/errors.rs
@@ -22,6 +22,11 @@ pub enum DistError {
     #[error(transparent)]
     Io(#[from] std::io::Error),
 
+    /// random axoasset error
+    #[error(transparent)]
+    #[diagnostic(transparent)]
+    Asset(#[from] axoasset::AxoassetError),
+
     /// User declined to update cargo-dist, refuse to make progress
     #[error(
         "to update your cargo-dist config you must use the version your project is configured for"
diff --git a/cargo-dist/src/init.rs b/cargo-dist/src/init.rs
index 53517da1..d5ff06b6 100644
--- a/cargo-dist/src/init.rs
+++ b/cargo-dist/src/init.rs
@@ -155,6 +155,7 @@ fn get_new_dist_metadata(
             windows_archive: None,
             unix_archive: None,
             npm_scope: None,
+            checksum: None,
         }
     };
 
@@ -509,6 +510,9 @@ fn update_toml_metadata(workspace_toml: &mut toml_edit::Document, meta: &DistMet
     const DESC_NPM_SCOPE: &str =
         "# A namespace to use when publishing this package to the npm registry\n";
 
+    const KEY_CHECKSUM: &str = "checksum";
+    const DESC_CHECKSUM: &str = "# Checksums to generate for each App\n";
+
     let workspace = workspace_toml["workspace"].or_insert(toml_edit::table());
     if let Some(t) = workspace.as_table_mut() {
         t.set_implicit(true)
@@ -632,6 +636,14 @@ fn update_toml_metadata(workspace_toml: &mut toml_edit::Document, meta: &DistMet
             .set_prefix(DESC_NPM_SCOPE);
     }
 
+    if let Some(val) = &meta.checksum {
+        table.insert(KEY_CHECKSUM, value(val.ext()));
+        table
+            .key_decor_mut(KEY_CHECKSUM)
+            .unwrap()
+            .set_prefix(DESC_CHECKSUM);
+    }
+
     table
         .decor_mut()
         .set_prefix("\n# Config for 'cargo dist'\n");
diff --git a/cargo-dist/src/lib.rs b/cargo-dist/src/lib.rs
index a9aec55b..fd3ee769 100644
--- a/cargo-dist/src/lib.rs
+++ b/cargo-dist/src/lib.rs
@@ -217,8 +217,15 @@ fn manifest_artifact(
             description = Some(info.desc.clone());
             kind = cargo_dist_schema::ArtifactKind::Installer;
         }
+        ArtifactKind::Checksum(_) => {
+            install_hint = None;
+            description = None;
+            kind = cargo_dist_schema::ArtifactKind::Checksum;
+        }
     };
 
+    let checksum = artifact.checksum.map(|idx| dist.artifact(idx).id.clone());
+
     cargo_dist_schema::Artifact {
         name: Some(artifact.id.clone()),
         path: if cfg.no_local_paths {
@@ -231,6 +238,7 @@ fn manifest_artifact(
         description,
         assets,
         kind,
+        checksum,
     }
 }
 
@@ -251,9 +259,53 @@ fn run_build_step(dist_graph: &DistGraph, target: &BuildStep) -> Result<()> {
             src_path,
             dest_path,
             zip_style,
-        }) => zip_dir(src_path, dest_path, zip_style),
+            dir_name,
+        }) => zip_dir(src_path, dest_path, zip_style, dir_name),
         BuildStep::GenerateInstaller(installer) => generate_installer(dist_graph, installer),
+        BuildStep::Checksum(ChecksumImpl {
+            checksum,
+            src_path,
+            dest_path,
+        }) => Ok(generate_checksum(checksum, src_path, dest_path)?),
+    }
+}
+
+/// Generate a checksum for the src_path to dest_path
+fn generate_checksum(
+    checksum: &ChecksumStyle,
+    src_path: &Utf8Path,
+    dest_path: &Utf8Path,
+) -> DistResult<()> {
+    use sha2::Digest;
+    use std::fmt::Write;
+
+    let file_bytes = axoasset::LocalAsset::load_bytes(src_path.as_str())?;
+
+    let hash = match checksum {
+        ChecksumStyle::Sha256 => {
+            let mut hasher = sha2::Sha256::new();
+            hasher.update(&file_bytes);
+            hasher.finalize().as_slice().to_owned()
+        }
+        ChecksumStyle::Sha512 => {
+            let mut hasher = sha2::Sha512::new();
+            hasher.update(&file_bytes);
+            hasher.finalize().as_slice().to_owned()
+        }
+        ChecksumStyle::False => {
+            unreachable!()
+        }
+    };
+    let mut output = String::new();
+    for byte in hash {
+        write!(&mut output, "{:02x}", byte).unwrap();
     }
+    axoasset::LocalAsset::write_new(
+        &output,
+        dest_path.file_name().unwrap(),
+        dest_path.parent().unwrap().as_str(),
+    )?;
+    Ok(())
 }
 
 /// Build a cargo target
@@ -454,17 +506,26 @@ fn copy_dir(_src_path: &Utf8Path, _dest_path: &Utf8Path) -> Result<()> {
     todo!("copy_dir isn't implemented yet")
 }
 
-fn zip_dir(src_path: &Utf8Path, dest_path: &Utf8Path, zip_style: &ZipStyle) -> Result<()> {
+fn zip_dir(
+    src_path: &Utf8Path,
+    dest_path: &Utf8Path,
+    zip_style: &ZipStyle,
+    dir_name: &str,
+) -> Result<()> {
     match zip_style {
-        ZipStyle::Zip => really_zip_dir(src_path, dest_path),
-        ZipStyle::Tar(compression) => tar_dir(src_path, dest_path, compression),
+        ZipStyle::Zip => really_zip_dir(src_path, dest_path, dir_name),
+        ZipStyle::Tar(compression) => tar_dir(src_path, dest_path, compression, dir_name),
     }
 }
 
-fn tar_dir(src_path: &Utf8Path, dest_path: &Utf8Path, compression: &CompressionImpl) -> Result<()> {
+fn tar_dir(
+    src_path: &Utf8Path,
+    dest_path: &Utf8Path,
+    compression: &CompressionImpl,
+    dir_name: &str,
+) -> Result<()> {
     // Set up the archive/compression
     // The contents of the zip (e.g. a tar)
-    let dir_name = src_path.file_name().unwrap();
     let zip_contents_name = format!("{dir_name}.tar");
     let final_zip_file = File::create(dest_path)
         .into_diagnostic()
@@ -552,7 +613,10 @@ fn tar_dir(src_path: &Utf8Path, dest_path: &Utf8Path, compression: &CompressionI
     Ok(())
 }
 
-fn really_zip_dir(src_path: &Utf8Path, dest_path: &Utf8Path) -> Result<()> {
+fn really_zip_dir(src_path: &Utf8Path, dest_path: &Utf8Path, _dir_name: &str) -> Result<()> {
+    // FIXME: we don't respect dir_name in that we don't add an extra wrapper dir
+    // ...but now things expect that and honestly that's more convenient for how
+    // we use these (installers --strip-prefix=1 our tarballs but there's no such option for unzip).
     // Set up the archive/compression
     let final_zip_file = File::create(dest_path)
         .into_diagnostic()
diff --git a/cargo-dist/src/main.rs b/cargo-dist/src/main.rs
index 89af3bf1..68bdd5f1 100644
--- a/cargo-dist/src/main.rs
+++ b/cargo-dist/src/main.rs
@@ -56,22 +56,14 @@ fn print_human(out: &mut Term, manifest: &DistManifest) -> Result<(), std::io::E
         )?;
         for artifact_id in &release.artifacts {
             let artifact = &manifest.artifacts[artifact_id];
-            // Print out the name or path of the artifact (path is more useful by noisier)
-            if let Some(path) = &artifact.path {
-                // Try to highlight the actual filename for easier scanning
-                let path = Utf8PathBuf::from(path);
-                let file = path.file_name().unwrap();
-                let parent = path.as_str().strip_suffix(file);
-                if let Some(parent) = parent {
-                    write!(out, "    {}", parent)?;
-                    writeln!(out, "{}", out.style().green().apply_to(file))?;
-                } else {
-                    write!(out, "    {}", out.style().green().apply_to(path))?;
-                }
-            } else if let Some(name) = &artifact.name {
-                writeln!(out, "    {}", out.style().green().apply_to(name))?;
+            if let cargo_dist_schema::ArtifactKind::Checksum = &artifact.kind {
+                // Don't print shasums at top-level
+                continue;
             }
 
+            write!(out, "    ")?;
+            print_human_artifact_path(out, artifact)?;
+
             // Print out all the binaries first, those are the money!
             for asset in &artifact.assets {
                 if let Some(path) = &asset.path {
@@ -103,7 +95,36 @@ fn print_human(out: &mut Term, manifest: &DistManifest) -> Result<(), std::io::E
             if printed_asset {
                 writeln!(out)?;
             }
+
+            // Mention the presence of a checksum if it exists
+            if let Some(checksum_id) = &artifact.checksum {
+                let checksum_artifact = &manifest.artifacts[checksum_id];
+                write!(out, "      [checksum] ")?;
+                print_human_artifact_path(out, checksum_artifact)?;
+            }
+        }
+    }
+    Ok(())
+}
+
+fn print_human_artifact_path(
+    out: &mut Term,
+    artifact: &cargo_dist_schema::Artifact,
+) -> Result<(), std::io::Error> {
+    // Print out the name or path of the artifact (path is more useful by noisier)
+    if let Some(path) = &artifact.path {
+        // Try to highlight the actual filename for easier scanning
+        let path = Utf8PathBuf::from(path);
+        let file = path.file_name().unwrap();
+        let parent = path.as_str().strip_suffix(file);
+        if let Some(parent) = parent {
+            write!(out, "{}", parent)?;
+            writeln!(out, "{}", out.style().green().apply_to(file))?;
+        } else {
+            write!(out, "{}", out.style().green().apply_to(path))?;
         }
+    } else if let Some(name) = &artifact.name {
+        writeln!(out, "{}", out.style().green().apply_to(name))?;
     }
     Ok(())
 }
diff --git a/cargo-dist/src/tasks.rs b/cargo-dist/src/tasks.rs
index 0a6aeb33..2f4230b2 100644
--- a/cargo-dist/src/tasks.rs
+++ b/cargo-dist/src/tasks.rs
@@ -216,6 +216,13 @@ pub struct DistMetadata {
     #[serde(skip_serializing_if = "Option::is_none")]
     #[serde(rename = "npm-scope")]
     pub npm_scope: Option<String>,
+
+    /// A scope to prefix npm packages with (@ should be included).
+    ///
+    /// This is required if you're using an npm installer.
+    #[serde(default)]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub checksum: Option<ChecksumStyle>,
 }
 
 impl DistMetadata {
@@ -255,6 +262,9 @@ impl DistMetadata {
         if self.npm_scope.is_none() {
             self.npm_scope = workspace_config.npm_scope.clone();
         }
+        if self.checksum.is_none() {
+            self.checksum = workspace_config.checksum;
+        }
         self.include
             .extend(workspace_config.include.iter().cloned());
     }
@@ -446,6 +456,8 @@ pub enum BuildStep {
     Zip(ZipDirStep),
     /// Generate some kind of installer
     GenerateInstaller(InstallerImpl),
+    /// Checksum a file
+    Checksum(ChecksumImpl),
     // FIXME: For macos universal builds we'll want
     // Lipo(LipoStep)
 }
@@ -483,6 +495,8 @@ pub struct ZipDirStep {
     pub src_path: Utf8PathBuf,
     /// The final file path for the output zip
     pub dest_path: Utf8PathBuf,
+    /// The name of the dir the tarball/zip will contain
+    pub dir_name: String,
     /// The kind of zip/tarball to make
     pub zip_style: ZipStyle,
 }
@@ -505,6 +519,42 @@ pub struct CopyDirStep {
     pub dest_path: Utf8PathBuf,
 }
 
+/// Create a checksum
+#[derive(Debug, Clone)]
+pub struct ChecksumImpl {
+    /// the checksumming algorithm
+    pub checksum: ChecksumStyle,
+    /// of this file
+    pub src_path: Utf8PathBuf,
+    /// and write it to here
+    pub dest_path: Utf8PathBuf,
+}
+
+/// A checksumming algorithm
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub enum ChecksumStyle {
+    /// sha256sum (using the sha2 crate)
+    #[serde(rename = "sha256")]
+    Sha256,
+    /// sha512sum (using the sha2 crate)
+    #[serde(rename = "sha512")]
+    Sha512,
+    /// Do not checksum
+    #[serde(rename = "false")]
+    False,
+}
+
+impl ChecksumStyle {
+    /// Get the extension of a checksum
+    pub fn ext(self) -> &'static str {
+        match self {
+            ChecksumStyle::Sha256 => "sha256",
+            ChecksumStyle::Sha512 => "sha512",
+            ChecksumStyle::False => "false",
+        }
+    }
+}
+
 /// A kind of symbols (debuginfo)
 #[derive(Copy, Clone, Debug)]
 pub enum SymbolKind {
@@ -552,6 +602,8 @@ pub struct Artifact {
     pub required_binaries: FastMap<BinaryIdx, Utf8PathBuf>,
     /// The kind of artifact this is
     pub kind: ArtifactKind,
+    /// A checksum for this artifact, if any
+    pub checksum: Option<ArtifactIdx>,
 }
 
 /// Info about an archive (zip/tarball) that should be made. Currently this is always part
@@ -587,6 +639,8 @@ pub enum ArtifactKind {
     Symbols(Symbols),
     /// An installer
     Installer(InstallerImpl),
+    /// A checksum
+    Checksum(ChecksumImpl),
 }
 
 /// An ExecutableZip Artifact
@@ -644,6 +698,8 @@ pub struct Release {
     pub windows_archive: ZipStyle,
     /// Archive format to use on non-windows
     pub unix_archive: ZipStyle,
+    /// Style of checksum to produce
+    pub checksum: ChecksumStyle,
     /// Static assets that should be included in bundles like executable-zips
     pub static_assets: Vec<(StaticAssetKind, Utf8PathBuf)>,
 }
@@ -940,6 +996,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
         let unix_archive = package_config
             .unix_archive
             .unwrap_or(ZipStyle::Tar(CompressionImpl::Xzip));
+        let checksum = package_config.checksum.unwrap_or(ChecksumStyle::Sha256);
 
         // Add static assets
         let mut static_assets = vec![];
@@ -981,6 +1038,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             windows_archive,
             unix_archive,
             static_assets,
+            checksum,
         });
         idx
     }
@@ -1059,6 +1117,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
         // Create an executable-zip for each Variant
         let release = self.release(to_release);
         let variants = release.variants.clone();
+        let checksum = release.checksum;
         for variant_idx in variants {
             let (zip_artifact, built_assets) =
                 self.make_executable_zip_for_variant(to_release, variant_idx);
@@ -1067,9 +1126,45 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             for (binary, dest_path) in built_assets {
                 self.require_binary(zip_artifact_idx, variant_idx, binary, dest_path);
             }
+
+            if checksum != ChecksumStyle::False {
+                self.add_artifact_checksum(variant_idx, zip_artifact_idx, checksum);
+            }
         }
     }
 
+    fn add_artifact_checksum(
+        &mut self,
+        to_variant: ReleaseVariantIdx,
+        artifact_idx: ArtifactIdx,
+        checksum: ChecksumStyle,
+    ) -> ArtifactIdx {
+        let artifact = self.artifact(artifact_idx);
+        let checksum_artifact = {
+            let checksum_ext = checksum.ext();
+            let checksum_id = format!("{}.{}", artifact.id, checksum_ext);
+            let checksum_path = artifact.file_path.parent().unwrap().join(&checksum_id);
+            Artifact {
+                id: checksum_id,
+                kind: ArtifactKind::Checksum(ChecksumImpl {
+                    checksum,
+                    src_path: artifact.file_path.clone(),
+                    dest_path: checksum_path.clone(),
+                }),
+
+                target_triples: artifact.target_triples.clone(),
+                archive: None,
+                file_path: checksum_path,
+                required_binaries: Default::default(),
+                // Who checksums the checksummers...
+                checksum: None,
+            }
+        };
+        let checksum_idx = self.add_local_artifact(to_variant, checksum_artifact);
+        self.artifact_mut(artifact_idx).checksum = Some(checksum_idx);
+        checksum_idx
+    }
+
     /// Make an executable zip for a variant, but don't yet integrate it into the graph
     ///
     /// This is useful for installers which want to know about *potential* executable zips
@@ -1119,6 +1214,8 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                     static_assets,
                 }),
                 kind: ArtifactKind::ExecutableZip(ExecutableZip {}),
+                // May get filled in later
+                checksum: None,
             },
             built_assets,
         )
@@ -1163,6 +1260,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                     file_path: artifact_path.clone(),
                     required_binaries: FastMap::new(),
                     kind: ArtifactKind::Symbols(Symbols { kind: symbol_kind }),
+                    checksum: None,
                 };
 
                 // FIXME: strictly speaking a binary could plausibly be shared between Releases,
@@ -1246,6 +1344,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             archive: None,
             file_path: artifact_path.clone(),
             required_binaries: FastMap::new(),
+            checksum: None,
             kind: ArtifactKind::Installer(InstallerImpl::Shell(InstallerInfo {
                 dest_path: artifact_path,
                 app_name: release.app_name.clone(),
@@ -1314,6 +1413,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             file_path: artifact_path.clone(),
             required_binaries: FastMap::new(),
             archive: None,
+            checksum: None,
             kind: ArtifactKind::Installer(InstallerImpl::Powershell(InstallerInfo {
                 dest_path: artifact_path,
                 app_name: release.app_name.clone(),
@@ -1411,13 +1511,15 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             id: artifact_name,
             target_triples: target_triples.into_iter().collect(),
             archive: Some(Archive {
-                dir_name,
+                // npm specifically expects the dir inside the tarball to be called "package"
+                dir_name: "package".to_owned(),
                 dir_path: dir_path.clone(),
                 zip_style,
                 static_assets,
             }),
             file_path: artifact_path.clone(),
             required_binaries: FastMap::new(),
+            checksum: None,
             kind: ArtifactKind::Installer(InstallerImpl::Npm(NpmInstallerInfo {
                 npm_package_name,
                 npm_package_version,
@@ -1503,6 +1605,9 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                     // Installer generation is complex enough that they just get monolithic impls
                     build_steps.push(BuildStep::GenerateInstaller(installer.clone()));
                 }
+                ArtifactKind::Checksum(checksum) => {
+                    build_steps.push(BuildStep::Checksum(checksum.clone()));
+                }
             }
 
             if let Some(archive) = &artifact.archive {
@@ -1528,6 +1633,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                 build_steps.push(BuildStep::Zip(ZipDirStep {
                     src_path: artifact_dir.to_owned(),
                     dest_path: artifact.file_path.clone(),
+                    dir_name: archive.dir_name.clone(),
                     zip_style: archive.zip_style,
                 }));
             }
@@ -1701,6 +1807,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                 match &artifact.kind {
                     ArtifactKind::ExecutableZip(zip) => bundles.push((artifact, zip)),
                     ArtifactKind::Symbols(syms) => symbols.push((artifact, syms)),
+                    ArtifactKind::Checksum(_) => {}
                     ArtifactKind::Installer(installer) => {
                         global_installers.push((artifact, installer))
                     }
@@ -1714,6 +1821,7 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                     match &artifact.kind {
                         ArtifactKind::ExecutableZip(zip) => bundles.push((artifact, zip)),
                         ArtifactKind::Symbols(syms) => symbols.push((artifact, syms)),
+                        ArtifactKind::Checksum(_) => {}
                         ArtifactKind::Installer(installer) => {
                             local_installers.push((artifact, installer))
                         }
@@ -1729,10 +1837,11 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                     | InstallerImpl::Npm(NpmInstallerInfo { inner: info, .. })) = details;
 
                     writeln!(&mut gh_body, "### {}\n", info.desc).unwrap();
-                    writeln!(&mut gh_body, "```shell\n{}\n```\n", info.hint).unwrap();
+                    writeln!(&mut gh_body, "```sh\n{}\n```\n", info.hint).unwrap();
                 }
             }
 
+            // TODO: checksums...
             let other_artifacts: Vec<_> = bundles
                 .iter()
                 .map(|i| i.0)
@@ -1742,8 +1851,8 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
             if !other_artifacts.is_empty() && download_url.is_some() {
                 let download_url = download_url.as_ref().unwrap();
                 writeln!(gh_body, "## Download {heading_suffix}\n",).unwrap();
-                gh_body.push_str("| target | kind | download |\n");
-                gh_body.push_str("|--------|------|----------|\n");
+                gh_body.push_str("|        |        |\n");
+                gh_body.push_str("|--------|--------|\n");
 
                 for artifact in other_artifacts {
                     let mut targets = String::new();
@@ -1755,15 +1864,17 @@ impl<'pkg_graph> DistGraphBuilder<'pkg_graph> {
                         targets.push_str(target);
                         multi_target = true;
                     }
-                    let kind = match artifact.kind {
-                        ArtifactKind::ExecutableZip(_) => "tarball",
-                        ArtifactKind::Symbols(_) => "symbols",
-                        ArtifactKind::Installer(_) => "installer",
-                    };
                     let name = &artifact.id;
                     let artifact_download_url = format!("{download_url}/{name}");
                     let download = format!("[{name}]({artifact_download_url})");
-                    writeln!(&mut gh_body, "| {targets} | {kind} | {download} |").unwrap();
+                    let checksum = if let Some(checksum_idx) = artifact.checksum {
+                        let checksum_name = &self.artifact(checksum_idx).id;
+                        let checksum_download_url = format!("{download_url}/{checksum_name}");
+                        format!("[checksum]({checksum_download_url})")
+                    } else {
+                        String::new()
+                    };
+                    writeln!(&mut gh_body, "| {download} | {checksum} |").unwrap();
                 }
                 writeln!(&mut gh_body).unwrap();
             }


Test patch:
diff --git a/cargo-dist/tests/snapshots/cli_tests__manifest.snap b/cargo-dist/tests/snapshots/cli_tests__manifest.snap
index 7f0143e5..318fe38a 100644
--- a/cargo-dist/tests/snapshots/cli_tests__manifest.snap
+++ b/cargo-dist/tests/snapshots/cli_tests__manifest.snap
@@ -18,9 +18,13 @@ stdout:
         "cargo-dist-installer.sh",
         "cargo-dist-installer.ps1",
         "cargo-dist-aarch64-apple-darwin.tar.xz",
+        "cargo-dist-aarch64-apple-darwin.tar.xz.sha256",
         "cargo-dist-x86_64-apple-darwin.tar.xz",
+        "cargo-dist-x86_64-apple-darwin.tar.xz.sha256",
         "cargo-dist-x86_64-pc-windows-msvc.zip",
-        "cargo-dist-x86_64-unknown-linux-gnu.tar.xz"
+        "cargo-dist-x86_64-pc-windows-msvc.zip.sha256",
+        "cargo-dist-x86_64-unknown-linux-gnu.tar.xz",
+        "cargo-dist-x86_64-unknown-linux-gnu.tar.xz.sha256"
       ]
     }
   ],
@@ -57,6 +61,14 @@ stdout:
           "path": "cargo-dist",
           "kind": "executable"
         }
+      ],
+      "checksum": "cargo-dist-aarch64-apple-darwin.tar.xz.sha256"
+    },
+    "cargo-dist-aarch64-apple-darwin.tar.xz.sha256": {
+      "name": "cargo-dist-aarch64-apple-darwin.tar.xz.sha256",
+      "kind": "checksum",
+      "target_triples": [
+        "aarch64-apple-darwin"
       ]
     },
     "cargo-dist-installer.ps1": {
@@ -111,6 +123,14 @@ stdout:
           "path": "cargo-dist",
           "kind": "executable"
         }
+      ],
+      "checksum": "cargo-dist-x86_64-apple-darwin.tar.xz.sha256"
+    },
+    "cargo-dist-x86_64-apple-darwin.tar.xz.sha256": {
+      "name": "cargo-dist-x86_64-apple-darwin.tar.xz.sha256",
+      "kind": "checksum",
+      "target_triples": [
+        "x86_64-apple-darwin"
       ]
     },
     "cargo-dist-x86_64-pc-windows-msvc.zip": {
@@ -145,6 +165,14 @@ stdout:
           "path": "cargo-dist.exe",
           "kind": "executable"
         }
+      ],
+      "checksum": "cargo-dist-x86_64-pc-windows-msvc.zip.sha256"
+    },
+    "cargo-dist-x86_64-pc-windows-msvc.zip.sha256": {
+      "name": "cargo-dist-x86_64-pc-windows-msvc.zip.sha256",
+      "kind": "checksum",
+      "target_triples": [
+        "x86_64-pc-windows-msvc"
       ]
     },
     "cargo-dist-x86_64-unknown-linux-gnu.tar.xz": {
@@ -179,6 +207,14 @@ stdout:
           "path": "cargo-dist",
           "kind": "executable"
         }
+      ],
+      "checksum": "cargo-dist-x86_64-unknown-linux-gnu.tar.xz.sha256"
+    },
+    "cargo-dist-x86_64-unknown-linux-gnu.tar.xz.sha256": {
+      "name": "cargo-dist-x86_64-unknown-linux-gnu.tar.xz.sha256",
+      "kind": "checksum",
+      "target_triples": [
+        "x86_64-unknown-linux-gnu"
       ]
     }
   }