1use crate::registry::DEFAULT_REGISTRY_NAME;
4use anyhow::{anyhow, bail, Context, Result};
5use semver::{Version, VersionReq};
6use serde::{de::IntoDeserializer, Deserialize, Serialize};
7use std::{
8 fs::{File, OpenOptions},
9 io::{self, Read, Seek, SeekFrom, Write},
10 path::{Path, PathBuf},
11};
12use toml_edit::{DocumentMut, Item, Value};
13use wasm_pkg_client::{ContentDigest, PackageRef};
14
15const LOCK_FILE_VERSION: i64 = 1;
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(rename_all = "kebab-case")]
21pub struct LockedPackage {
22 pub name: PackageRef,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub registry: Option<String>,
29 #[serde(rename = "version", default, skip_serializing_if = "Vec::is_empty")]
34 pub versions: Vec<LockedPackageVersion>,
35}
36
37impl LockedPackage {
38 pub fn key(&self) -> (&str, &str, &str) {
40 (
41 self.name.namespace().as_ref(),
42 self.name.name().as_ref(),
43 self.registry.as_deref().unwrap_or(DEFAULT_REGISTRY_NAME),
44 )
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct LockedPackageVersion {
51 pub requirement: String,
53 pub version: Version,
55 pub digest: ContentDigest,
57}
58
59impl LockedPackageVersion {
60 pub fn key(&self) -> &str {
62 &self.requirement
63 }
64}
65
66#[derive(Clone, Copy, Debug)]
68pub struct LockFileResolver<'a>(&'a LockFile);
69
70impl<'a> LockFileResolver<'a> {
71 pub fn new(lock_file: &'a LockFile) -> Self {
73 Self(lock_file)
74 }
75
76 pub fn resolve(
82 &'a self,
83 registry: &str,
84 package_ref: &PackageRef,
85 requirement: &VersionReq,
86 ) -> Result<Option<&'a LockedPackageVersion>> {
87 if let Some(pkg) = self
88 .0
89 .packages
90 .binary_search_by_key(
91 &(
92 package_ref.namespace().as_ref(),
93 package_ref.name().as_ref(),
94 registry,
95 ),
96 LockedPackage::key,
97 )
98 .ok()
99 .map(|i| &self.0.packages[i])
100 {
101 if let Ok(index) = pkg
102 .versions
103 .binary_search_by_key(&requirement.to_string().as_str(), LockedPackageVersion::key)
104 {
105 let locked = &pkg.versions[index];
106 log::info!("dependency package `{package_ref}` from registry `{registry}` with requirement `{requirement}` was resolved by the lock file to version {version}", version = locked.version);
107 return Ok(Some(locked));
108 }
109 }
110
111 log::info!("dependency package `{package_ref}` from registry `{registry}` with requirement `{requirement}` was not in the lock file");
112 Ok(None)
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
121#[serde(rename_all = "kebab-case")]
122pub struct LockFile {
123 pub version: i64,
127 #[serde(rename = "package", default, skip_serializing_if = "Vec::is_empty")]
131 pub packages: Vec<LockedPackage>,
132}
133
134impl LockFile {
135 pub fn new(packages: impl Into<Vec<LockedPackage>>) -> Self {
139 Self {
140 version: LOCK_FILE_VERSION,
141 packages: packages.into(),
142 }
143 }
144
145 pub fn read(mut file: &File) -> Result<Self> {
147 let mut contents = String::new();
148 file.read_to_string(&mut contents)?;
149
150 let document: DocumentMut = contents.parse()?;
151
152 match document.as_table().get("version") {
153 Some(Item::Value(Value::Integer(v))) => {
154 if *v.value() != LOCK_FILE_VERSION {
155 bail!(
156 "unsupported file format version {version}",
157 version = v.value()
158 );
159 }
160
161 }
163 Some(_) => bail!("file format version is not an integer"),
164 None => bail!("missing file format version"),
165 }
166
167 Self::deserialize(document.into_deserializer()).context("invalid file format")
168 }
169
170 pub fn write(&self, mut file: &File, app: &str) -> Result<()> {
174 let content = toml_edit::ser::to_string_pretty(&self)?;
175
176 file.set_len(0)?;
177 write!(file, "# This file is automatically generated by {app}.\n# It is not intended for manual editing.\n")?;
178 file.write_all(content.as_bytes())?;
179
180 Ok(())
181 }
182}
183
184impl Default for LockFile {
185 fn default() -> Self {
186 Self {
187 version: LOCK_FILE_VERSION,
188 packages: Vec::new(),
189 }
190 }
191}
192
193#[derive(Debug)]
195pub struct FileLock {
196 file: File,
197 path: PathBuf,
198}
199
200#[derive(Debug, Copy, Clone, Eq, PartialEq)]
201enum Access {
202 Shared,
203 Exclusive,
204}
205
206impl FileLock {
207 pub fn path(&self) -> &Path {
209 &self.path
210 }
211
212 pub fn try_open_rw(path: impl Into<PathBuf>) -> Result<Option<Self>> {
224 Self::open(
225 path.into(),
226 OpenOptions::new().read(true).write(true).create(true),
227 Access::Exclusive,
228 true,
229 )
230 }
231
232 pub fn open_rw(path: impl Into<PathBuf>) -> Result<Self> {
245 Ok(Self::open(
246 path.into(),
247 OpenOptions::new().read(true).write(true).create(true),
248 Access::Exclusive,
249 false,
250 )?
251 .unwrap())
252 }
253
254 pub fn try_open_ro(path: impl Into<PathBuf>) -> Result<Option<Self>> {
266 Self::open(
267 path.into(),
268 OpenOptions::new().read(true),
269 Access::Shared,
270 true,
271 )
272 }
273
274 pub fn open_ro(path: impl Into<PathBuf>) -> Result<Self> {
286 Ok(Self::open(
287 path.into(),
288 OpenOptions::new().read(true),
289 Access::Shared,
290 false,
291 )?
292 .unwrap())
293 }
294
295 fn open(
296 path: PathBuf,
297 opts: &OpenOptions,
298 access: Access,
299 try_lock: bool,
300 ) -> Result<Option<Self>> {
301 let file = opts
305 .open(&path)
306 .or_else(|e| {
307 if e.kind() == io::ErrorKind::NotFound && access == Access::Exclusive {
308 std::fs::create_dir_all(path.parent().unwrap())?;
309 Ok(opts.open(&path)?)
310 } else {
311 Err(anyhow::Error::from(e))
312 }
313 })
314 .with_context(|| format!("failed to open `{path}`", path = path.display()))?;
315
316 let lock = Self { file, path };
317
318 if is_on_nfs_mount(&lock.path) {
329 return Ok(Some(lock));
330 }
331
332 let res = match (access, try_lock) {
333 (Access::Shared, true) => sys::try_lock_shared(&lock.file),
334 (Access::Exclusive, true) => sys::try_lock_exclusive(&lock.file),
335 (Access::Shared, false) => sys::lock_shared(&lock.file),
336 (Access::Exclusive, false) => sys::lock_exclusive(&lock.file),
337 };
338
339 return match res {
340 Ok(_) => Ok(Some(lock)),
341
342 Err(e) if sys::error_unsupported(&e) => Ok(Some(lock)),
346
347 Err(e) if try_lock && sys::error_contended(&e) => Ok(None),
349
350 Err(e) => Err(anyhow!(e).context(format!(
351 "failed to lock file `{path}`",
352 path = lock.path.display()
353 ))),
354 };
355
356 #[cfg(all(target_os = "linux", not(target_env = "musl")))]
357 fn is_on_nfs_mount(path: &Path) -> bool {
358 use std::ffi::CString;
359 use std::mem;
360 use std::os::unix::prelude::*;
361
362 let path = match CString::new(path.as_os_str().as_bytes()) {
363 Ok(path) => path,
364 Err(_) => return false,
365 };
366
367 unsafe {
368 let mut buf: libc::statfs = mem::zeroed();
369 let r = libc::statfs(path.as_ptr(), &mut buf);
370
371 r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
372 }
373 }
374
375 #[cfg(any(not(target_os = "linux"), target_env = "musl"))]
376 fn is_on_nfs_mount(_path: &Path) -> bool {
377 false
378 }
379 }
380
381 pub fn file(&self) -> &File {
383 &self.file
384 }
385}
386
387impl Read for FileLock {
388 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
389 self.file().read(buf)
390 }
391}
392
393impl Seek for FileLock {
394 fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
395 self.file().seek(to)
396 }
397}
398
399impl Write for FileLock {
400 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
401 self.file().write(buf)
402 }
403
404 fn flush(&mut self) -> io::Result<()> {
405 self.file().flush()
406 }
407}
408
409impl Drop for FileLock {
410 fn drop(&mut self) {
411 let _ = sys::unlock(&self.file);
412 }
413}
414
415#[cfg(unix)]
416mod sys {
417 use std::fs::File;
418 use std::io::{Error, Result};
419 use std::os::unix::io::AsRawFd;
420
421 pub(super) fn lock_shared(file: &File) -> Result<()> {
422 flock(file, libc::LOCK_SH)
423 }
424
425 pub(super) fn lock_exclusive(file: &File) -> Result<()> {
426 flock(file, libc::LOCK_EX)
427 }
428
429 pub(super) fn try_lock_shared(file: &File) -> Result<()> {
430 flock(file, libc::LOCK_SH | libc::LOCK_NB)
431 }
432
433 pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
434 flock(file, libc::LOCK_EX | libc::LOCK_NB)
435 }
436
437 pub(super) fn unlock(file: &File) -> Result<()> {
438 flock(file, libc::LOCK_UN)
439 }
440
441 pub(super) fn error_contended(err: &Error) -> bool {
442 err.raw_os_error().map_or(false, |x| x == libc::EWOULDBLOCK)
443 }
444
445 pub(super) fn error_unsupported(err: &Error) -> bool {
446 match err.raw_os_error() {
447 #[allow(unreachable_patterns)]
450 Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
451 Some(libc::ENOSYS) => true,
452 _ => false,
453 }
454 }
455
456 #[cfg(not(target_os = "solaris"))]
457 fn flock(file: &File, flag: libc::c_int) -> Result<()> {
458 let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
459 if ret < 0 {
460 Err(Error::last_os_error())
461 } else {
462 Ok(())
463 }
464 }
465
466 #[cfg(target_os = "solaris")]
467 fn flock(file: &File, flag: libc::c_int) -> Result<()> {
468 let mut flock = libc::flock {
470 l_type: 0,
471 l_whence: 0,
472 l_start: 0,
473 l_len: 0,
474 l_sysid: 0,
475 l_pid: 0,
476 l_pad: [0, 0, 0, 0],
477 };
478 flock.l_type = if flag & libc::LOCK_UN != 0 {
479 libc::F_UNLCK
480 } else if flag & libc::LOCK_EX != 0 {
481 libc::F_WRLCK
482 } else if flag & libc::LOCK_SH != 0 {
483 libc::F_RDLCK
484 } else {
485 panic!("unexpected flock() operation")
486 };
487
488 let mut cmd = libc::F_SETLKW;
489 if (flag & libc::LOCK_NB) != 0 {
490 cmd = libc::F_SETLK;
491 }
492
493 let ret = unsafe { libc::fcntl(file.as_raw_fd(), cmd, &flock) };
494
495 if ret < 0 {
496 Err(Error::last_os_error())
497 } else {
498 Ok(())
499 }
500 }
501}
502
503#[cfg(windows)]
504mod sys {
505 use std::fs::File;
506 use std::io::{Error, Result};
507 use std::mem;
508 use std::os::windows::io::AsRawHandle;
509
510 use windows_sys::Win32::Foundation::HANDLE;
511 use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION};
512 use windows_sys::Win32::Storage::FileSystem::{
513 LockFileEx, UnlockFile, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
514 };
515
516 pub(super) fn lock_shared(file: &File) -> Result<()> {
517 lock_file(file, 0)
518 }
519
520 pub(super) fn lock_exclusive(file: &File) -> Result<()> {
521 lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
522 }
523
524 pub(super) fn try_lock_shared(file: &File) -> Result<()> {
525 lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
526 }
527
528 pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
529 lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
530 }
531
532 pub(super) fn error_contended(err: &Error) -> bool {
533 err.raw_os_error()
534 .map_or(false, |x| x == ERROR_LOCK_VIOLATION as i32)
535 }
536
537 pub(super) fn error_unsupported(err: &Error) -> bool {
538 err.raw_os_error()
539 .map_or(false, |x| x == ERROR_INVALID_FUNCTION as i32)
540 }
541
542 pub(super) fn unlock(file: &File) -> Result<()> {
543 unsafe {
544 let ret = UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0);
545 if ret == 0 {
546 Err(Error::last_os_error())
547 } else {
548 Ok(())
549 }
550 }
551 }
552
553 fn lock_file(file: &File, flags: u32) -> Result<()> {
554 unsafe {
555 let mut overlapped = mem::zeroed();
556 let ret = LockFileEx(
557 file.as_raw_handle() as HANDLE,
558 flags,
559 0,
560 !0,
561 !0,
562 &mut overlapped,
563 );
564 if ret == 0 {
565 Err(Error::last_os_error())
566 } else {
567 Ok(())
568 }
569 }
570 }
571}