cargo_component_core/
lock.rs

1//! Module for the lock file implementation.
2
3use 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
15/// The file format version of the lock file.
16const LOCK_FILE_VERSION: i64 = 1;
17
18/// Represents a locked package in a lock file.
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(rename_all = "kebab-case")]
21pub struct LockedPackage {
22    /// The name of the locked package.
23    pub name: PackageRef,
24    /// The registry the package was resolved from.
25    ///
26    /// Defaults to the default registry if not specified.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub registry: Option<String>,
29    /// The locked versions of a package.
30    ///
31    /// A package may have multiple locked versions if more than one
32    /// version requirement was specified for the package in `wit.toml`.
33    #[serde(rename = "version", default, skip_serializing_if = "Vec::is_empty")]
34    pub versions: Vec<LockedPackageVersion>,
35}
36
37impl LockedPackage {
38    /// Gets the key used in sorting and searching the package list.
39    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/// Represents version information for a locked package.
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct LockedPackageVersion {
51    /// The version requirement used to resolve this version.
52    pub requirement: String,
53    /// The version the package is locked to.
54    pub version: Version,
55    /// The digest of the package contents.
56    pub digest: ContentDigest,
57}
58
59impl LockedPackageVersion {
60    /// Gets the sort key for the locked package version.
61    pub fn key(&self) -> &str {
62        &self.requirement
63    }
64}
65
66/// Represents a resolver for a lock file.
67#[derive(Clone, Copy, Debug)]
68pub struct LockFileResolver<'a>(&'a LockFile);
69
70impl<'a> LockFileResolver<'a> {
71    /// Creates a new lock file resolver for the given workspace and lock file.
72    pub fn new(lock_file: &'a LockFile) -> Self {
73        Self(lock_file)
74    }
75
76    /// Resolves a package from the lock file.
77    ///
78    /// Returns `Ok(None)` if the package cannot be resolved.
79    ///
80    /// Fails if the package cannot be resolved and the lock file is not allowed to be updated.
81    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/// Represents a resolved dependency lock file.
117///
118/// This is a TOML file that contains the resolved dependency information from
119/// a previous build.
120#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
121#[serde(rename_all = "kebab-case")]
122pub struct LockFile {
123    /// The version of the lock file.
124    ///
125    /// Currently this is always `1`.
126    pub version: i64,
127    /// The locked dependencies in the lock file.
128    ///
129    /// This list is sorted by the key of the locked package.
130    #[serde(rename = "package", default, skip_serializing_if = "Vec::is_empty")]
131    pub packages: Vec<LockedPackage>,
132}
133
134impl LockFile {
135    /// Constructs a new lock file from a list of locked packages.
136    ///
137    /// It is expected that the packages will be already sorted.
138    pub fn new(packages: impl Into<Vec<LockedPackage>>) -> Self {
139        Self {
140            version: LOCK_FILE_VERSION,
141            packages: packages.into(),
142        }
143    }
144
145    /// Reads the lock file from the given file object.
146    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                // In the future, we should convert between supported versions here.
162            }
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    /// Writes the lock file to the given file object.
171    ///
172    /// The app name is used to generate a header comment.
173    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/// Implements a file lock.
194#[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    /// Gets the path associated with the file lock.
208    pub fn path(&self) -> &Path {
209        &self.path
210    }
211
212    /// Attempts to acquire exclusive access to a file, returning the locked
213    /// version of a file.
214    ///
215    /// This function will create a file at `path` if it doesn't already exist
216    /// (including intermediate directories), and then it will try to acquire an
217    /// exclusive lock on `path`.
218    ///
219    /// If the lock cannot be immediately acquired, `Ok(None)` is returned.
220    ///
221    /// The returned file can be accessed to look at the path and also has
222    /// read/write access to the underlying file.
223    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    /// Opens exclusive access to a file, returning the locked version of a
233    /// file.
234    ///
235    /// This function will create a file at `path` if it doesn't already exist
236    /// (including intermediate directories), and then it will acquire an
237    /// exclusive lock on `path`.
238    ///
239    /// If the lock cannot be acquired, this function will block until it is
240    /// acquired.
241    ///
242    /// The returned file can be accessed to look at the path and also has
243    /// read/write access to the underlying file.
244    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    /// Attempts to acquire shared access to a file, returning the locked version
255    /// of a file.
256    ///
257    /// This function will fail if `path` doesn't already exist, but if it does
258    /// then it will acquire a shared lock on `path`.
259    ///
260    /// If the lock cannot be immediately acquired, `Ok(None)` is returned.
261    ///
262    /// The returned file can be accessed to look at the path and also has read
263    /// access to the underlying file. Any writes to the file will return an
264    /// error.
265    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    /// Opens shared access to a file, returning the locked version of a file.
275    ///
276    /// This function will fail if `path` doesn't already exist, but if it does
277    /// then it will acquire a shared lock on `path`.
278    ///
279    /// If the lock cannot be acquired, this function will block until it is
280    /// acquired.
281    ///
282    /// The returned file can be accessed to look at the path and also has read
283    /// access to the underlying file. Any writes to the file will return an
284    /// error.
285    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        // If we want an exclusive lock then if we fail because of NotFound it's
302        // likely because an intermediate directory didn't exist, so try to
303        // create the directory and then continue.
304        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        // File locking on Unix is currently implemented via `flock`, which is known
319        // to be broken on NFS. We could in theory just ignore errors that happen on
320        // NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
321        // forever**, even if the "non-blocking" flag is passed!
322        //
323        // As a result, we just skip all file locks entirely on NFS mounts. That
324        // should avoid calling any `flock` functions at all, and it wouldn't work
325        // there anyway.
326        //
327        // [1]: https://github.com/rust-lang/cargo/issues/2615
328        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            // In addition to ignoring NFS which is commonly not working we also
343            // just ignore locking on file systems that look like they don't
344            // implement file locking.
345            Err(e) if sys::error_unsupported(&e) => Ok(Some(lock)),
346
347            // Check to see if it was a contention error
348            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    /// Returns the underlying file handle of this lock.
382    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            // Unfortunately, depending on the target, these may or may not be the same.
448            // For targets in which they are the same, the duplicate pattern causes a warning.
449            #[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        // Solaris lacks flock(), so try to emulate using fcntl()
469        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}