Fork of daniellemaywood.uk/gleam — Wasm codegen work
11 kB
330 lines
1use camino::Utf8Path;
2use debug_ignore::DebugIgnore;
3use flate2::read::GzDecoder;
4use futures::future;
5use hexpm::{ApiError, version::Version};
6use tar::Archive;
7
8use crate::{
9 Error, Result,
10 io::{FileSystemReader, FileSystemWriter, HttpClient, TarUnpacker},
11 manifest::{ManifestPackage, ManifestPackageSource},
12 paths::{self, ProjectPaths},
13};
14
15pub const HEXPM_PUBLIC_KEY: &[u8] = b"-----BEGIN PUBLIC KEY-----
16MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApqREcFDt5vV21JVe2QNB
17Edvzk6w36aNFhVGWN5toNJRjRJ6m4hIuG4KaXtDWVLjnvct6MYMfqhC79HAGwyF+
18IqR6Q6a5bbFSsImgBJwz1oadoVKD6ZNetAuCIK84cjMrEFRkELtEIPNHblCzUkkM
193rS9+DPlnfG8hBvGi6tvQIuZmXGCxF/73hU0/MyGhbmEjIKRtG6b0sJYKelRLTPW
20XgK7s5pESgiwf2YC/2MGDXjAJfpfCd0RpLdvd4eRiXtVlE9qO9bND94E7PgQ/xqZ
21J1i2xWFndWa6nfFnRxZmCStCOZWYYPlaxr+FZceFbpMwzTNs4g3d4tLNUcbKAIH4
220wIDAQAB
23-----END PUBLIC KEY-----
24";
25
26fn key_name(hostname: &str) -> String {
27 format!("gleam-{hostname}")
28}
29
30pub async fn publish_package<Http: HttpClient>(
31 release_tarball: Vec<u8>,
32 version: String,
33 api_key: &str,
34 config: &hexpm::Config,
35 replace: bool,
36 http: &Http,
37) -> Result<()> {
38 tracing::info!("Publishing package, replace: {}", replace);
39 let request = hexpm::api_publish_package_request(release_tarball, api_key, config, replace);
40 let response = http.send(request).await?;
41 hexpm::api_publish_package_response(response).map_err(|e| match e {
42 ApiError::NotReplacing => Error::HexPublishReplaceRequired { version },
43 err => Error::hex(err),
44 })
45}
46
47pub async fn transfer_owner<Http: HttpClient>(
48 api_key: &str,
49 package_name: String,
50 new_owner_username_or_email: String,
51 config: &hexpm::Config,
52 http: &Http,
53) -> Result<()> {
54 tracing::info!(
55 "Transferring ownership of `{}` to {}",
56 package_name,
57 new_owner_username_or_email
58 );
59 let request = hexpm::api_transfer_owner_request(
60 &package_name,
61 &new_owner_username_or_email,
62 api_key,
63 config,
64 );
65 let response = http.send(request).await?;
66 hexpm::api_transfer_owner_response(response).map_err(Error::hex)
67}
68
69#[derive(Debug, strum::EnumString, strum::VariantNames, Clone, Copy, PartialEq, Eq)]
70#[strum(serialize_all = "lowercase")]
71pub enum RetirementReason {
72 Other,
73 Invalid,
74 Security,
75 Deprecated,
76 Renamed,
77}
78
79impl RetirementReason {
80 pub fn to_library_enum(&self) -> hexpm::RetirementReason {
81 match self {
82 RetirementReason::Other => hexpm::RetirementReason::Other,
83 RetirementReason::Invalid => hexpm::RetirementReason::Invalid,
84 RetirementReason::Security => hexpm::RetirementReason::Security,
85 RetirementReason::Deprecated => hexpm::RetirementReason::Deprecated,
86 RetirementReason::Renamed => hexpm::RetirementReason::Renamed,
87 }
88 }
89}
90
91pub async fn retire_release<Http: HttpClient>(
92 package: &str,
93 version: &str,
94 reason: RetirementReason,
95 message: Option<&str>,
96 api_key: &str,
97 config: &hexpm::Config,
98 http: &Http,
99) -> Result<()> {
100 tracing::info!(package=%package, version=%version, "retiring_hex_release");
101 let request = hexpm::api_retire_release_request(
102 package,
103 version,
104 reason.to_library_enum(),
105 message,
106 api_key,
107 config,
108 );
109 let response = http.send(request).await?;
110 hexpm::api_retire_release_response(response).map_err(Error::hex)
111}
112
113pub async fn unretire_release<Http: HttpClient>(
114 package: &str,
115 version: &str,
116 api_key: &str,
117 config: &hexpm::Config,
118 http: &Http,
119) -> Result<()> {
120 tracing::info!(package=%package, version=%version, "retiring_hex_release");
121 let request = hexpm::api_unretire_release_request(package, version, api_key, config);
122 let response = http.send(request).await?;
123 hexpm::api_unretire_release_response(response).map_err(Error::hex)
124}
125
126pub async fn create_api_key<Http: HttpClient>(
127 hostname: &str,
128 username: &str,
129 password: &str,
130 config: &hexpm::Config,
131 http: &Http,
132) -> Result<String> {
133 tracing::info!("Creating API key with Hex");
134 let request =
135 hexpm::api_create_api_key_request(username, password, &key_name(hostname), config);
136 let response = http.send(request).await?;
137 hexpm::api_create_api_key_response(response).map_err(Error::hex)
138}
139
140pub async fn remove_api_key<Http: HttpClient>(
141 hostname: &str,
142 config: &hexpm::Config,
143 auth_key: &str,
144 http: &Http,
145) -> Result<()> {
146 tracing::info!("Deleting API key from Hex");
147 let request = hexpm::api_remove_api_key_request(&key_name(hostname), auth_key, config);
148 let response = http.send(request).await?;
149 hexpm::api_remove_api_key_response(response).map_err(Error::hex)
150}
151
152#[derive(Debug)]
153pub struct Downloader {
154 fs_reader: DebugIgnore<Box<dyn FileSystemReader>>,
155 fs_writer: DebugIgnore<Box<dyn FileSystemWriter>>,
156 http: DebugIgnore<Box<dyn HttpClient>>,
157 untar: DebugIgnore<Box<dyn TarUnpacker>>,
158 hex_config: hexpm::Config,
159 paths: ProjectPaths,
160}
161
162impl Downloader {
163 pub fn new(
164 fs_reader: Box<dyn FileSystemReader>,
165 fs_writer: Box<dyn FileSystemWriter>,
166 http: Box<dyn HttpClient>,
167 untar: Box<dyn TarUnpacker>,
168 paths: ProjectPaths,
169 ) -> Self {
170 Self {
171 fs_reader: DebugIgnore(fs_reader),
172 fs_writer: DebugIgnore(fs_writer),
173 http: DebugIgnore(http),
174 untar: DebugIgnore(untar),
175 hex_config: hexpm::Config::new(),
176 paths,
177 }
178 }
179
180 pub async fn ensure_package_downloaded(
181 &self,
182 package: &ManifestPackage,
183 ) -> Result<bool, Error> {
184 let outer_checksum = match &package.source {
185 ManifestPackageSource::Hex { outer_checksum } => outer_checksum,
186 _ => {
187 panic!("Attempt to download non-hex package from hex")
188 }
189 };
190
191 let tarball_path = paths::global_package_cache_package_tarball(
192 &package.name,
193 &package.version.to_string(),
194 );
195 if self.fs_reader.is_file(&tarball_path) {
196 tracing::info!(
197 package = package.name.as_str(),
198 version = %package.version,
199 "package_in_cache"
200 );
201 return Ok(false);
202 }
203 tracing::info!(
204 package = &package.name.as_str(),
205 version = %package.version,
206 "downloading_package_to_cache"
207 );
208
209 let request = hexpm::repository_get_package_tarball_request(
210 &package.name,
211 &package.version.to_string(),
212 None,
213 &self.hex_config,
214 );
215 let response = self.http.send(request).await?;
216
217 let tarball = hexpm::repository_get_package_tarball_response(response, &outer_checksum.0)
218 .map_err(|error| Error::DownloadPackageError {
219 package_name: package.name.to_string(),
220 package_version: package.version.to_string(),
221 error: error.to_string(),
222 })?;
223 self.fs_writer.write_bytes(&tarball_path, &tarball)?;
224 Ok(true)
225 }
226
227 pub async fn ensure_package_in_build_directory(
228 &self,
229 package: &ManifestPackage,
230 ) -> Result<bool> {
231 let _ = self.ensure_package_downloaded(package).await?;
232 self.extract_package_from_cache(&package.name, &package.version)
233 }
234
235 // It would be really nice if this was async but the library is sync
236 pub fn extract_package_from_cache(&self, name: &str, version: &Version) -> Result<bool> {
237 let contents_path = Utf8Path::new("contents.tar.gz");
238 let destination = self.paths.build_packages_package(name);
239
240 // If the directory already exists then there's nothing for us to do
241 if self.fs_reader.is_directory(&destination) {
242 tracing::info!(package = name, "Package already in build directory");
243 return Ok(false);
244 }
245
246 tracing::info!(package = name, "writing_package_to_target");
247 let tarball = paths::global_package_cache_package_tarball(name, &version.to_string());
248 let reader = self.fs_reader.reader(&tarball)?;
249 let mut archive = Archive::new(reader);
250
251 // Find the source code from within the outer tarball
252 for entry in self.untar.entries(&mut archive)? {
253 let file = entry.map_err(Error::expand_tar)?;
254
255 let path = file.header().path().map_err(Error::expand_tar)?;
256 if path.as_ref() == contents_path {
257 // Expand this inner source code and write to the file system
258 let archive = Archive::new(GzDecoder::new(file));
259 let result = self.untar.unpack(&destination, archive);
260
261 // If we failed to expand the tarball remove any source code
262 // that was partially written so that we don't mistakenly think
263 // the operation succeeded next time we run.
264 return match result {
265 Ok(()) => Ok(true),
266 Err(err) => {
267 self.fs_writer.delete_directory(&destination)?;
268 Err(err)
269 }
270 };
271 }
272 }
273
274 Err(Error::ExpandTar {
275 error: "Unable to locate Hex package contents.tar.gz".into(),
276 })
277 }
278
279 pub async fn download_hex_packages<'a, Packages: Iterator<Item = &'a ManifestPackage>>(
280 &self,
281 packages: Packages,
282 project_name: &str,
283 ) -> Result<()> {
284 let futures = packages
285 .filter(|package| project_name != package.name)
286 .map(|package| self.ensure_package_in_build_directory(package));
287
288 // Run the futures to download the packages concurrently
289 let results = future::join_all(futures).await;
290
291 // Count the number of packages downloaded while checking for errors
292 for result in results {
293 let _ = result?;
294 }
295 Ok(())
296 }
297}
298
299pub async fn publish_documentation<Http: HttpClient>(
300 name: &str,
301 version: &Version,
302 archive: Vec<u8>,
303 api_key: &str,
304 config: &hexpm::Config,
305 http: &Http,
306) -> Result<()> {
307 tracing::info!("publishing_documentation");
308 let request =
309 hexpm::api_publish_docs_request(name, &version.to_string(), archive, api_key, config)
310 .map_err(Error::hex)?;
311 let response = http.send(request).await?;
312 hexpm::api_publish_docs_response(response).map_err(Error::hex)
313}
314
315pub async fn get_package_release<Http: HttpClient>(
316 name: &str,
317 version: &Version,
318 config: &hexpm::Config,
319 http: &Http,
320) -> Result<hexpm::Release<hexpm::ReleaseMeta>> {
321 let version = version.to_string();
322 tracing::info!(
323 name = name,
324 version = version.as_str(),
325 "looking_up_package_release"
326 );
327 let request = hexpm::api_get_package_release_request(name, &version, None, config);
328 let response = http.send(request).await?;
329 hexpm::api_get_package_release_response(response).map_err(Error::hex)
330}