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