Fork of daniellemaywood.uk/gleam — Wasm codegen work
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}