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