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