Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / licence-bundler / src / licence_bundler.gleam
6.6 kB 237 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2026 The Gleam contributors 3 4import gleam/bool 5import gleam/io 6import gleam/list 7import gleam/result 8import gleam/set.{type Set} 9import gleam/string 10import gleam/string_tree 11import htmb 12import shellout 13import simplifile 14import tom 15 16pub fn main() -> Nil { 17 let gleam_crates = determine_gleam_crates() 18 let active_licences = determine_licences() 19 20 // This command makes cargo print all the dependencies with their name, 21 // version, and licence SPDX expression. Unfortunately it is a tree and not a 22 // list, so it requires some further parsing to use. 23 // 24 // The output is too verbose to show an example here, so run this command if 25 // you want to see it yourself: 26 // 27 // cargo tree --format "{p} {l}" --color never --edges no-dev,no-build,no-proc 28 // 29 let assert Ok(output) = 30 shellout.command( 31 run: "cargo", 32 with: [ 33 "tree", 34 "--format", 35 "{p} {l}", 36 "--color", 37 "never", 38 "--edges", 39 "no-dev,no-build,no-proc-macro", 40 ], 41 in: "..", 42 opt: [], 43 ) 44 45 let rust_dependencies = 46 output 47 |> string.split("\n") 48 |> list.filter(fn(line) { line != "" }) 49 |> list.map(trim_line_prefix) 50 |> list.filter_map(dependency_information(_, gleam_crates, active_licences)) 51 |> list.unique 52 |> list.sort(fn(a, b) { 53 string.compare( 54 a.name <> a.licence_identifier, 55 b.name <> b.licence_identifier, 56 ) 57 }) 58 59 let assert Ok(licences) = 60 active_licences 61 |> set.to_list 62 |> list.sort(string.compare) 63 |> list.try_map(fn(identifier) { 64 simplifile.read("../licences/" <> identifier <> ".txt") 65 |> result.map(Licence(identifier:, text: _)) 66 }) 67 68 let h = htmb.h 69 let t = htmb.text 70 71 let licence_slug = fn(identifier) { string.replace(identifier, " ", "-") } 72 let licence_link = fn(licence_identifier) { 73 let slug = licence_slug(licence_identifier) 74 h("a", [#("href", "#" <> slug)], [t(licence_identifier)]) 75 } 76 77 let html = 78 h("html", [#("lang", "en")], [ 79 h("head", [], []), 80 h("body", [], [ 81 h("h1", [], [t("Gleam dependency licences")]), 82 h("section", [], [ 83 h("h2", [], [t("Rust dependency packages")]), 84 h_ul(rust_dependencies, fn(package) { 85 let licences = package.used_licences 86 assert licences != [] as { package.name <> " missing licences" } 87 let licences = 88 list.map(licences, licence_link) 89 |> list.intersperse(t(", ")) 90 let url = 91 "https://crates.io/crates/" 92 <> package.name 93 <> "/" 94 <> package.version 95 [ 96 h("a", [#("href", url)], [ 97 t(package.name <> "@" <> package.version), 98 ]), 99 t(" using "), 100 ..licences 101 ] 102 }), 103 ]), 104 h("section", [], [ 105 h("h2", [], [t("Licences")]), 106 h_ul(licences, fn(licence) { 107 let id = licence.identifier 108 [ 109 h("h3", [#("id", licence_slug(id))], [t(id)]), 110 h("pre", [], [t(licence.text)]), 111 ] 112 }), 113 ]), 114 ]), 115 ]) 116 |> htmb.render_page 117 |> string_tree.to_string 118 119 let assert Ok(_) = simplifile.write("../gleam-licences.html", html) 120 io.println("Written ../gleam-licences.html") 121 122 Nil 123} 124 125fn h_ul(items: List(a), mapper: fn(a) -> List(htmb.Html)) -> htmb.Html { 126 htmb.h("ul", [], list.map(items, fn(item) { htmb.h("li", [], mapper(item)) })) 127} 128 129fn determine_licences() -> Set(String) { 130 let assert Ok(files) = simplifile.read_directory("../licences") 131 files 132 |> list.map(string.remove_suffix(_, ".txt")) 133 |> set.from_list 134} 135 136fn determine_gleam_crates() -> Set(String) { 137 let assert Ok(toml) = simplifile.read("../Cargo.toml") 138 let assert Ok(config) = tom.parse(toml) 139 let assert Ok(crates) = tom.get_array(config, ["workspace", "members"]) 140 let assert Ok(paths) = list.try_map(crates, tom.as_string) 141 let crates = 142 list.map(paths, fn(path) { 143 let assert Ok(toml) = simplifile.read("../" <> path <> "/Cargo.toml") 144 let assert Ok(config) = tom.parse(toml) 145 let assert Ok(name) = tom.get_string(config, ["package", "name"]) 146 name 147 }) 148 set.from_list(crates) 149} 150 151type Crate { 152 Crate( 153 name: String, 154 version: String, 155 /// The SPDX expression that says what licences can be used for this crate. 156 licence_identifier: String, 157 /// The licences from that SPDX expression that we are using. The expression 158 /// can contain OR, so not all the licences referenced may be in use by us. 159 used_licences: List(String), 160 ) 161} 162 163type Licence { 164 Licence(identifier: String, text: String) 165} 166 167fn dependency_information( 168 line: String, 169 gleam_crates: Set(String), 170 active_licences: Set(String), 171) -> Result(Crate, Nil) { 172 let line = string.remove_suffix(line, " (*)") 173 let assert [name, version, ..licence_parts] = string.split(line, " ") 174 use <- bool.guard(set.contains(gleam_crates, name), return: Error(Nil)) 175 let version = string.remove_prefix(version, "v") 176 let licence_identifier = string.join(licence_parts, " ") 177 let used_licences = 178 licence_identifier 179 |> string.split(" OR ") 180 |> list.flat_map(string.split(_, " AND ")) 181 |> list.flat_map(string.split(_, "/")) 182 |> list.map(string.remove_prefix(_, "(")) 183 |> list.map(string.remove_suffix(_, ")")) 184 |> list.map(string.remove_suffix(_, "+")) 185 |> list.filter(set.contains(active_licences, _)) 186 assert used_licences != [] 187 as { string.inspect(line) <> " parsed to no used licences" } 188 Ok(Crate(name:, version:, licence_identifier:, used_licences:)) 189} 190 191/// Convert lines like these: 192/// 193/// ```txt 194/// │ └── wasm-bindgen v0.2.106 MIT OR Apache-2.0 195/// ``` 196/// 197/// into lines like these 198/// 199/// ```txt 200/// wasm-bindgen v0.2.106 MIT OR Apache-2.0 201/// ``` 202/// 203fn trim_line_prefix(line: String) -> String { 204 case line { 205 " " <> line | "" <> line | "" <> line | "" <> line | "" <> line -> 206 trim_line_prefix(line) 207 208 "" 209 | "a" <> _ 210 | "b" <> _ 211 | "c" <> _ 212 | "d" <> _ 213 | "e" <> _ 214 | "f" <> _ 215 | "g" <> _ 216 | "h" <> _ 217 | "i" <> _ 218 | "j" <> _ 219 | "k" <> _ 220 | "l" <> _ 221 | "m" <> _ 222 | "n" <> _ 223 | "o" <> _ 224 | "p" <> _ 225 | "q" <> _ 226 | "r" <> _ 227 | "s" <> _ 228 | "t" <> _ 229 | "u" <> _ 230 | "v" <> _ 231 | "w" <> _ 232 | "x" <> _ 233 | "y" <> _ 234 | "z" <> _ -> line 235 _ -> panic as { "Unexpected line prefix: " <> string.inspect(line) } 236 } 237}