diff --git a/Cargo.lock b/Cargo.lock index b2586ee..df61a35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1301,6 +1301,18 @@ dependencies = [ "sha-1 0.8.2", ] +[[package]] +name = "pfacts" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83abebdb324c30f176d449513f0134bafbf976d5279c6554742599e3996d1629" +dependencies = [ + "rand 0.7.3", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "pin-project" version = "0.4.22" @@ -2507,6 +2519,7 @@ dependencies = [ "color-eyre", "comrak", "envy", + "eyre", "glob", "go_vanity", "hyper", @@ -2516,12 +2529,15 @@ dependencies = [ "log", "mime", "patreon", + "pfacts", "pretty_env_logger", "prometheus", "rand 0.7.3", + "reqwest", "ructe", "serde", "serde_dhall", + "serde_json", "serde_yaml", "sitemap", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index f43230d..eb6cd67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,12 @@ patreon = { path = "./lib/patreon" } [build-dependencies] ructe = { version = "0.12", features = ["warp02"] } +[dev-dependencies] +pfacts = "0" +serde_json = "1" +eyre = "0.6" +reqwest = { version = "0.10", features = ["json"] } + [workspace] members = [ "./lib/go_vanity", diff --git a/blog/rust-crates-go-stdlib-2020-09-27.markdown b/blog/rust-crates-go-stdlib-2020-09-27.markdown new file mode 100644 index 0000000..f44b9e7 --- /dev/null +++ b/blog/rust-crates-go-stdlib-2020-09-27.markdown @@ -0,0 +1,617 @@ +--- +title: Rust Crates that do What the Go Standard library Does +date: 2020-09-27 +tags: + - rust +--- + +# Rust Crates that do What the Go Standard library Does + +One of Go's greatest strengths is how batteries-included the standard library +is. You can do most of what you need to do with only the standard library. On +the other hand, Rust's standard library is severely lacking by comparison. +However, the community has capitalized on this and been working on a bunch of +batteries that you can include in your rust projects. I'm going to cover a bunch +of them in this post in a few sections. + +[A lot of these are actually used to help make this blog site +work!](conversation://Mara/hacker) + +## Logging + +Go has logging out of the box with package [`log`](https://godoc.org/log). +Package `log` is a very uncontroversial logger. It does what it says it does and +with little fuss. However it does not include a lot of niceties like logging +levels and context-aware values. + +In Rust, we have the [`log`](https://docs.rs/log/) crate which is a very simple +interface. It uses the `error!`, `warn!`, `info!`, `debug!` and `trace!` macros +which correlate to the highest and lowest levers. If you want to use `log` in a +Rust crate, you can add it to your `Cargo.toml` file like this: + +```toml +[dependencies] +log = "0.4" +``` + +Then you can use it in your Rust code like this: + +```rust +use log::{error, warn, info, debug, trace}; + +fn main() { + trace!("starting main"); + debug!("debug message"); + info!("this is some information"); + warn!("oh no something bad is about to happen"); + error!("oh no it's an error"); +} +``` + +[Wait, where does that log to? I ran that example locally but I didn't see any +of the messages anywhere](conversation://Mara/wat) + +This is because the `log` crate doesn't directly log anything anywhere, it is a +facade that other packages build off of. +[`pretty_env_logger`](https://docs.rs/pretty_env_logger) is a commonly used +crate with the `log` facade. Let's add it to the program and work from there: + +```toml +[dependencies] +log = "0.4" +pretty_env_logger = "0.4" +``` + +Then let's enable it in our code: + +```rust +use log::{error, warn, info, debug, trace}; + +fn main() { + pretty_env_logger::init(); + + trace!("starting main"); + debug!("debug message"); + info!("this is some information"); + warn!("oh no something bad is about to happen"); + error!("oh no it's an error"); +} +``` + +And now let's run it with `RUST_LOG=trace`: + +```console +$ env RUST_LOG=trace cargo run --example logger_test + Finished dev [unoptimized + debuginfo] target(s) in 0.07s + Running `/home/cadey/code/christine.website/target/debug/logger_test` + TRACE logger_test > starting main + DEBUG logger_test > debug message + INFO logger_test > this is some information + WARN logger_test > oh no something bad is about to happen + ERROR logger_test > oh no it's an error +``` + +There are [many +other](https://docs.rs/log/0.4.11/log/#available-logging-implementations) +consumers of the log crate and implementing a consumer is easy should you want +to do more than `pretty_env_logger` can do on its own. However, I have found +that `pretty_env_logger` does just enough on its own. See its documentation for +more information. + +## Flags + +Go's standard library has the [`flag`](https://godoc.org/flag) package out of +the box. This package is incredibly basic, but is surprisingly capable in terms +of what you can actually do with it. A common thing to do is use flags for +configuration or other options, such as +[here](https://github.com/Xe/hlang/blob/44bb74efa6f124ca05483a527c0e735ce0fca143/main.go#L15-L22): + +```go +package main + +import "flag" + +var ( + program = flag.String("p", "", "h program to compile/run") + outFname = flag.String("o", "", "if specified, write the webassembly binary created by -p here") + watFname = flag.String("o-wat", "", "if specified, write the uncompiled webassembly created by -p here") + port = flag.String("port", "", "HTTP port to listen on") + writeTao = flag.Bool("koan", false, "if true, print the h koan and then exit") + writeVersion = flag.Bool("v", false, "if true, print the version of h and then exit") +) +``` + +This will make a few package-global variables that will contain the values of +the command-line arguments. + +In Rust, a commonly used command line parsing package is +[`structopt`](https://docs.rs/structopt). It works in a bit of a different way +than Go's `flag` package does though. `structopt` focuses on loading options into +a structure rather than into globally mutable variables. + +[Something you may notice in Rust-land is that globally mutable state is talked +about as if it is something to be avoided. It's not inherently bad, but it does +make things more likely to crash at runtime. In most cases, these global +variables with package `flag` are fine, but only if they are ever written to +before the program really starts to do what it needs to do. If they are ever +written to and read from dynamically at runtime, then you can get into a lot of +problems such as race +conditions.](conversation://Mara/hacker) + +Here's a quick example copied from [pa'i](https://github.com/Xe/pahi): + +```rust +#[derive(Debug, StructOpt)] +#[structopt( + name = "pa'i", + about = "A WebAssembly runtime in Rust meeting the Olin ABI." +)] +struct Opt { + /// Backend + #[structopt(short, long, default_value = "cranelift")] + backend: String, + + + /// Print syscalls on exit + #[structopt(short, long)] + function_log: bool, + + + /// Do not cache compiled code? + #[structopt(short, long)] + no_cache: bool, + + + /// Binary to run + #[structopt()] + fname: String, + + + /// Main function + #[structopt(short, long, default_value = "_start")] + entrypoint: String, + + + /// Arguments of the wasm child + #[structopt()] + args: Vec, +} +``` + +This has the Rust compiler generate the needed argument parsing code for you, so +you can just use the values as normal: + +```rust +fn main() { + let opt = Opt::from_args(); + debug!("args: {:?}", opt.args); +} +``` + +You can even handle subcommands with this, such as in +[palisade](https://github.com/lightspeed/palisade/blob/master/src/main.rs). This +package should handle just about everything you'd do with the `flag` package, +but will also work for cases where `flag` falls apart. + +## Errors + +Go's standard library has the [`error` +interface](https://godoc.org/builtin#error) which lets you create a type that +describes why functions fail to do what they intend. Rust has the [`Error` +trait](https://doc.rust-lang.org/std/error/trait.Error.html) which lets you also +create a type that describes why functions fail to do what they intend. + +In [my last post](https://christine.website/blog/TLDR-rust-2020-09-19) I +described [`eyre`](https://docs.rs/eyre) and the Result type. However, this time +we're going to dive into [`thiserror`](https://docs.rs/thiserror) for making our +own error type. Let's add `thiserror` to our crate: + +```toml +[dependencies] +thiserror = "1" +``` + +And then let's re-implement our `DivideByZero` error from the last post: + +```rust +use std::fmt; +use thiserror::Error; + +#[derive(Debug, Error)] +struct DivideByZero; + +impl fmt::Display for DivideByZero { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "cannot divide by zero") + } +} +``` + +The compiler made our error instance for us! It can even do that for more +complicated error types like this one that wraps a lot of other error cases and +error types in [maj](https://tulpa.dev/cadey/maj): + +```rust +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("TLS error: {0:?}")] + TLS(#[from] TLSError), + + #[error("URL error: {0:?}")] + URL(#[from] url::ParseError), + + #[error("Invalid DNS name: {0:?}")] + InvalidDNSName(#[from] webpki::InvalidDNSNameError), + + #[error("IO error: {0:?}")] + IO(#[from] std::io::Error), + + #[error("Response parsing error: {0:?}")] + ResponseParse(#[from] crate::ResponseError), + + #[error("Invalid URL scheme {0:?}")] + InvalidScheme(String), +} +``` + +[These `#[error("whatever")]` annotations will show up when the error message is +printed. See here for more +information on what details you can include here.](conversation://Mara/hacker) + +## Serialization / Deserialization + +Go has JSON encoding/decoding in its standard library via package +[`encoding/json`](https://godoc.org/encoding/json). This allows you to define +types that can be read from and write to JSON easily. Let's take this simple +JSON object representing a comment from some imaginary API as an example: + +```json +{ + "id": 31337, + "author": { + "id": 420, + "name": "Cadey" + }, + "body": "hahaha its is an laughter image", + "in_reply_to": 31335 +} +``` + +In Go you could write this as: + +```go +type Author struct { + ID int `json:"id"` + Name string `json:"name"` +} + +type Comment struct { + ID int `json:"id"` + Author Author `json:"author"` + Body string `json:"body"` + InReplyTo int `json:"in_reply_to"` +} +``` + +Rust does not have this capability out of the box, however there is a fantastic +framework available known as [serde](https://serde.rs/) which works across JSON +and every other serialization method that you can think of. Let's add serde and +its JSON support to our crate: + +```toml +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +``` + +[You might notice that the dependency line for serde is different here. Go's +JSON package works by using struct +tags as metadata, but Rust doesn't have these. We need to use Rust's derive +feature instead.](conversation://Mara/hacker) + +So, to use serde for our comment type, we would write Rust that looks like this: + +```rust +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Author { + pub id: i32, + pub name: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Comment { + pub id: i32, + pub author: Author, + pub body: String, + pub in_reply_to: i32, +} +``` + +And then we can load that from JSON using code like this: + +```rust +fn main() { + let data = r#" + { + "id": 31337, + "author": { + "id": 420, + "name": "Cadey" + }, + "body": "hahaha its is an laughter image", + "in_reply_to": 31335 + } + "#; + + let c: Comment = serde_json::from_str(data).expect("json to parse"); + println!("comment: {:#?}", c); +} +``` + +And you can use it like this: + +```console +$ cargo run --example json + Compiling xesite v2.0.1 (/home/cadey/code/christine.website) + Finished dev [unoptimized + debuginfo] target(s) in 0.43s + Running `target/debug/examples/json` +comment: Comment { + id: 31337, + author: Author { + id: 420, + name: "Cadey", + }, + body: "hahaha its is an laughter image", + in_reply_to: 31335, +} +``` + +## HTTP + +Many APIs expose their data over HTTP. Go has the +[`net/http`](https://godoc.org/net/http) package that acts as a production-grade +(Google uses this in production) HTTP client and server. This allows you to get +going with new projects very easily. The Rust standard library doesn't have this +out of the box, but there are some very convenient crates that can fill in the +blanks. + +### Client + +For an HTTP client, we can use [`reqwest`](https://docs.rs/reqwest). It can also +seamlessly integrate with serde to allow you to parse JSON from HTTP without any +issues. Let's add reqwest to our crate as well as [`tokio`](https://tokio.rs) to +act as an asynchronous runtime: + +```toml +[dependencies] +reqwest = { version = "0.10", features = ["json"] } +tokio = { version = "0.2", features = ["full"] } +``` + +[We need `tokio` because Rust doesn't ship with an asynchronous runtime by +default. Go does as a core part of the standard library (and arguably the +language), but `tokio` is about equivalent to most of the important things that +the Go runtime handles for you. This omission may seem annoying, but it makes it +easy for you to create a custom asynchronous runtime should you need +to.](conversation://Mara/hacker) + +And then let's integrate with that imaginary comment api at +[https://xena.greedo.xeserv.us/files/comment.json](https://xena.greedo.xeserv.us/files/comment.json): + +```rust +use eyre::Result; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Author { + pub id: i32, + pub name: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Comment { + pub id: i32, + pub author: Author, + pub body: String, + pub in_reply_to: i32, +} + +#[tokio::main] +async fn main() -> Result<()> { + let c: Comment = reqwest::get("https://xena.greedo.xeserv.us/files/comment.json") + .await? + .json() + .await?; + println!("comment: {:#?}", c); + + Ok(()) +} +``` + +And then let's run this: + +```console +$ cargo run --example http + Compiling xesite v2.0.1 (/home/cadey/code/christine.website) + Finished dev [unoptimized + debuginfo] target(s) in 2.20s + Running `target/debug/examples/http` +comment: Comment { + id: 31337, + author: Author { + id: 420, + name: "Cadey", + }, + body: "hahaha its is an laughter image", + in_reply_to: 31335, +} +``` + +[But what if the response status is not 200?](conversation://Mara/hmm) + +We can change the code to something like this: + +```rust +let c: Comment = reqwest::get("https://xena.greedo.xeserv.us/files/comment2.json") + .await? + .error_for_status()? + .json() + .await?; +``` + +And then when we run it we get an error back: + +```console +$ cargo run --example http_fail + Compiling xesite v2.0.1 (/home/cadey/code/christine.website) + Finished dev [unoptimized + debuginfo] target(s) in 1.84s + Running `/home/cadey/code/christine.website/target/debug/examples/http_fail` +Error: HTTP status client error (404 Not Found) for url (https://xena.greedo.xeserv.us/files/comment2.json) +``` + +This combined with the other features in `reqwest` give you an very capable HTTP +client that does even more than Go's HTTP client does out of the box. + +### Server + +As for HTTP servers though, let's take a look at [`warp`](https://docs.rs/warp). +`warp` is a HTTP server framework that builds on top of Rust's type system. +You can add warp to your dependencies like this: + +```toml +[dependencies] +warp = "0.2" +``` + +Let's take a look at its ["Hello, World" example](https://github.com/seanmonstar/warp/blob/master/examples/hello.rs): + +```rust +use warp::Filter; + +#[tokio::main] +async fn main() { + // GET /hello/warp => 200 OK with body "Hello, warp!" + let hello = warp::path!("hello" / String) + .map(|name| format!("Hello, {}!", name)); + + warp::serve(hello) + .run(([127, 0, 0, 1], 3030)) + .await; +} +``` + +We can then build up multiple routes with its `or` pattern: + +``` +let hello = warp::path!("hello" / String) + .map(|name| format!("Hello, {}!", name)); +let health = warp::path!(".within" / "health") + .map(|| "OK"); +let routes = hello.or(health); +``` + +And even inject other datatypes into your handlers with filters such as in the +[printer facts API server](https://tulpa.dev/cadey/printerfacts/src/branch/main/src/main.rs): + +``` +let fact = { + let facts = pfacts::make(); + warp::any().map(move || facts.clone()) +}; + +let fact_handler = warp::get() + .and(warp::path("fact")) + .and(fact.clone()) + .and_then(give_fact); +``` + +`warp` is an extremely capable HTTP server and can work across everything you +need for production-grade web apps. + +[The blog you are looking at right now is powered by +warp!](conversation://Mara/hacker) + +## Templating + +Go's standard library also includes HTML and plain text templating with its +packages [`html/template`](https://godoc.org/html/template) and +[`text/template`](https://godoc.org/text/template). There are many solutions for +templating HTML in Rust, but the one I like the most is +[`ructe`](https://docs.rs/ructe). `ructe` uses Cargo's +[build.rs](https://doc.rust-lang.org/cargo/reference/build-scripts.html) feature +to generate Rust code for its templates at compile time. This allows your HTML +templates to be compiled into the resulting application binary, allowing them to +render at ludicrous speeds. To use it, you need to add it to your +`build-dependencies` section of your `Cargo.toml`: + +```toml +[build-dependencies] +ructe = { version = "0.12", features = ["warp02"] } +``` + +You will also need to add the [`mime`](https://docs.rs/mime) crate to your +dependencies because the generated template code will require it at runtime. + +```toml +[dependencies] +mime = "0.3.0" +``` + +Once you've done this, create a new folder named `templates` in your current +working directory. Create a file called `hello.rs.html` and put the following in +it: + +```html +@(title: String, message: String) + + + + @title + + +

@title

+

@message

+ + +``` + +Now add the following to the bottom of your `main.rs` file: + +```rust +include!(concat!(env!("OUT_DIR"), "/templates.rs")); +``` + +And then use the template like this: + +```rust +use warp::{http::Response, Filter, Rejection, Reply}; + +async fn hello_html(message: String) -> Result { + Response::builder() + .html(|o| templates::index_html(o, "Hello".to_string(), message).unwrap().clone())) +} +``` + +And hook it up in your main function: + +```rust +let hello_html_rt = warp::path!("hello" / "html" / String) + .and_then(hello_html); + +let routes = hello_html_rt.or(health).or(hello); +``` + +For a more comprehensive example, check out the [printerfacts +server](https://tulpa.dev/cadey/printerfacts). It also shows how to handle 404 +responses and other things like that. + +--- + +Wow, this covered a lot. I've included most of the example code in the +[`examples`](https://github.com/Xe/site/tree/master/examples) folder of [this +site's GitHub repo](https://github.com/Xe/site). I hope it will help you on your +journey in Rust. This is documentation that I wish I had when I was learning +Rust. diff --git a/examples/http.rs b/examples/http.rs new file mode 100644 index 0000000..c1533a6 --- /dev/null +++ b/examples/http.rs @@ -0,0 +1,27 @@ +use eyre::Result; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Author { + pub id: i32, + pub name: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Comment { + pub id: i32, + pub author: Author, + pub body: String, + pub in_reply_to: i32, +} + +#[tokio::main] +async fn main() -> Result<()> { + let c: Comment = reqwest::get("https://xena.greedo.xeserv.us/files/comment.json") + .await? + .json() + .await?; + println!("comment: {:#?}", c); + + Ok(()) +} diff --git a/examples/http_fail.rs b/examples/http_fail.rs new file mode 100644 index 0000000..98e027a --- /dev/null +++ b/examples/http_fail.rs @@ -0,0 +1,28 @@ +use eyre::Result; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Author { + pub id: i32, + pub name: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Comment { + pub id: i32, + pub author: Author, + pub body: String, + pub in_reply_to: i32, +} + +#[tokio::main] +async fn main() -> Result<()> { + let c: Comment = reqwest::get("https://xena.greedo.xeserv.us/files/comment2.json") + .await? + .error_for_status()? + .json() + .await?; + println!("comment: {:#?}", c); + + Ok(()) +} diff --git a/examples/json.rs b/examples/json.rs new file mode 100644 index 0000000..bf128ed --- /dev/null +++ b/examples/json.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Author { + pub id: i32, + pub name: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Comment { + pub id: i32, + pub author: Author, + pub body: String, + pub in_reply_to: i32, +} + +fn main() { + let data = r#" + { + "id": 31337, + "author": { + "id": 420, + "name": "Cadey" + }, + "body": "hahaha its is an laughter image", + "in_reply_to": 31335 + } + "#; + + let c: Comment = serde_json::from_str(data).expect("json to parse"); + println!("comment: {:#?}", c); +} diff --git a/examples/logger_test.rs b/examples/logger_test.rs new file mode 100644 index 0000000..f917d66 --- /dev/null +++ b/examples/logger_test.rs @@ -0,0 +1,11 @@ +use log::{debug, error, info, trace, warn}; + +fn main() { + pretty_env_logger::init(); + + trace!("starting main"); + debug!("debug message"); + info!("this is some information"); + warn!("oh no something bad is about to happen"); + error!("oh no it's an error"); +} diff --git a/examples/warp.rs b/examples/warp.rs new file mode 100644 index 0000000..c705b47 --- /dev/null +++ b/examples/warp.rs @@ -0,0 +1,14 @@ +use warp::Filter; + +#[tokio::main] +async fn main() { + let hello = warp::path!("hello" / String) + .map(|name| format!("Hello, {}!", name)); + let health = warp::path!(".within" / "health") + .map(|| "OK"); + let routes = hello.or(health); + + warp::serve(routes) + .run(([0, 0, 0, 0], 3030)) + .await; +} diff --git a/nix/sources.json b/nix/sources.json index 8eb271a..8f412c2 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -5,10 +5,10 @@ "homepage": "", "owner": "justinwoo", "repo": "easy-dhall-nix", - "rev": "90957969850a44481c6e150350c56e8b53b29e1e", - "sha256": "1hsmp3cb0k554kh0jlfzpdzx2b8ndyh2gdykmw9hw41haaw16mmi", + "rev": "3e9101c5dfd69a9fc28fe4998aff378f91bfcb64", + "sha256": "1nsn1n4sx4za6jipcid1293rdw8lqgj9097s0khiij3fz0bzhrg9", "type": "tarball", - "url": "https://github.com/justinwoo/easy-dhall-nix/archive/90957969850a44481c6e150350c56e8b53b29e1e.tar.gz", + "url": "https://github.com/justinwoo/easy-dhall-nix/archive/3e9101c5dfd69a9fc28fe4998aff378f91bfcb64.tar.gz", "url_template": "https://github.com///archive/.tar.gz" }, "naersk": { @@ -29,10 +29,10 @@ "homepage": "https://github.com/nmattia/niv", "owner": "nmattia", "repo": "niv", - "rev": "fad2a6cbfb2e7cdebb7cb0ad2f5cc91e2c9bc06b", - "sha256": "0mghc1j0rd15spdjx81bayjqr0khc062cs25y5dcfzlxk4ynyc6m", + "rev": "29ddaaf4e099c3ac0647f5b652469dfc79cd3b53", + "sha256": "1va6myp07gkspgxfch8z3rs9nyvys6jmgzkys6a2c4j09qxp1bs0", "type": "tarball", - "url": "https://github.com/nmattia/niv/archive/fad2a6cbfb2e7cdebb7cb0ad2f5cc91e2c9bc06b.tar.gz", + "url": "https://github.com/nmattia/niv/archive/29ddaaf4e099c3ac0647f5b652469dfc79cd3b53.tar.gz", "url_template": "https://github.com///archive/.tar.gz" }, "nixpkgs": { @@ -41,10 +41,10 @@ "homepage": "https://github.com/NixOS/nixpkgs", "owner": "NixOS", "repo": "nixpkgs-channels", - "rev": "4aa5466cbc741097218a1c494a7b832a17d1967d", - "sha256": "0w6gg1hxcx83l8s83frxyjm5dwri06a3cy1i4358ny2lcrxq4qap", + "rev": "72b9660dc18ba347f7cd41a9504fc181a6d87dc3", + "sha256": "1cqgpw263bz261bgz34j6hiawi4hi6smwp6981yz375fx0g6kmss", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs-channels/archive/4aa5466cbc741097218a1c494a7b832a17d1967d.tar.gz", + "url": "https://github.com/NixOS/nixpkgs-channels/archive/72b9660dc18ba347f7cd41a9504fc181a6d87dc3.tar.gz", "url_template": "https://github.com///archive/.tar.gz" }, "xepkgs": {