@@ -20,6 +20,7 @@ const STATE_FILE_VERSION = 1;
20
20
const NODE_MODULES = `node_modules` as Filename ;
21
21
const DOT_BIN = `.bin` as Filename ;
22
22
const INSTALL_STATE_FILE = `.yarn-state.yml` as Filename ;
23
+ const MTIME_ACCURANCY = 1000 ;
23
24
24
25
type InstallState = { locatorMap : NodeModulesLocatorMap , locationTree : LocationTree , binSymlinks : BinSymlinkMap , nmMode : NodeModulesMode , mtimeMs : number } ;
25
26
type BinSymlinkMap = Map < PortablePath , Map < Filename , PortablePath > > ;
@@ -695,56 +696,67 @@ async function atomicFileWrite(tmpDir: PortablePath, dstPath: PortablePath, cont
695
696
}
696
697
}
697
698
698
- async function copyFilePromise ( { srcPath, dstPath, srcMode, globalHardlinksStore, baseFs, nmMode, digest} : { srcPath : PortablePath , dstPath : PortablePath , srcMode : number , globalHardlinksStore : PortablePath | null , baseFs : FakeFS < PortablePath > , nmMode : { value : NodeModulesMode } , digest ?: string } ) {
699
- if ( nmMode . value === NodeModulesMode . HARDLINKS_GLOBAL && globalHardlinksStore && digest ) {
700
- const contentFilePath = ppath . join ( globalHardlinksStore , digest . substring ( 0 , 2 ) as Filename , `${ digest . substring ( 2 ) } .dat` as Filename ) ;
699
+ async function copyFilePromise ( { srcPath, dstPath, entry, globalHardlinksStore, baseFs, nmMode} : { srcPath : PortablePath , dstPath : PortablePath , entry : DirEntry , globalHardlinksStore : PortablePath | null , baseFs : FakeFS < PortablePath > , nmMode : { value : NodeModulesMode } } ) {
700
+ if ( entry . kind === DirEntryKind . FILE ) {
701
+ if ( nmMode . value === NodeModulesMode . HARDLINKS_GLOBAL && globalHardlinksStore && entry . digest ) {
702
+ const contentFilePath = ppath . join ( globalHardlinksStore , entry . digest . substring ( 0 , 2 ) as Filename , `${ entry . digest . substring ( 2 ) } .dat` as Filename ) ;
701
703
702
- let doesContentFileExist ;
703
- try {
704
- const contentDigest = await hashUtils . checksumFile ( contentFilePath , { baseFs : xfs , algorithm : `sha1` } ) ;
705
- if ( contentDigest !== digest ) {
706
- // If file content was modified by the user, or corrupted, we first move it out of the way
707
- const tmpPath = ppath . join ( globalHardlinksStore , toFilename ( `${ crypto . randomBytes ( 16 ) . toString ( `hex` ) } .tmp` ) ) ;
708
- await xfs . renamePromise ( contentFilePath , tmpPath ) ;
704
+ let doesContentFileExist ;
705
+ try {
706
+ const stats = await xfs . statPromise ( contentFilePath ) ;
707
+
708
+ if ( stats && ( ! entry . mtimeMs || stats . mtimeMs > entry . mtimeMs || stats . mtimeMs < entry . mtimeMs - MTIME_ACCURANCY ) ) {
709
+ const contentDigest = await hashUtils . checksumFile ( contentFilePath , { baseFs : xfs , algorithm : `sha1` } ) ;
710
+ if ( contentDigest !== entry . digest ) {
711
+ // If file content was modified by the user, or corrupted, we first move it out of the way
712
+ const tmpPath = ppath . join ( globalHardlinksStore , toFilename ( `${ crypto . randomBytes ( 16 ) . toString ( `hex` ) } .tmp` ) ) ;
713
+ await xfs . renamePromise ( contentFilePath , tmpPath ) ;
714
+
715
+ // Then we overwrite the temporary file, thus restorting content of original file in all the linked projects
716
+ const content = await baseFs . readFilePromise ( srcPath ) ;
717
+ await xfs . writeFilePromise ( tmpPath , content ) ;
718
+
719
+ try {
720
+ // Then we try to move content file back on its place, if its still free
721
+ // If we fail here, it means that some other process or thread has created content file
722
+ // And this is okay, we will end up with two content files, but both with original content, unlucky files will have `.tmp` extension
723
+ await xfs . linkPromise ( tmpPath , contentFilePath ) ;
724
+ entry . mtimeMs = new Date ( ) . getTime ( ) ;
725
+ await xfs . unlinkPromise ( tmpPath ) ;
726
+ } catch ( e ) {
727
+ }
728
+ } else if ( ! entry . mtimeMs ) {
729
+ entry . mtimeMs = Math . ceil ( stats . mtimeMs ) ;
730
+ }
731
+ }
709
732
710
- // Then we overwrite the temporary file, thus restorting content of original file in all the linked projects
711
- const content = await baseFs . readFilePromise ( srcPath ) ;
712
- await xfs . writeFilePromise ( tmpPath , content ) ;
733
+ await xfs . linkPromise ( contentFilePath , dstPath ) ;
734
+ doesContentFileExist = true ;
735
+ } catch ( e ) {
736
+ doesContentFileExist = false ;
737
+ }
713
738
739
+ if ( ! doesContentFileExist ) {
740
+ const content = await baseFs . readFilePromise ( srcPath ) ;
741
+ await atomicFileWrite ( globalHardlinksStore , contentFilePath , content ) ;
742
+ entry . mtimeMs = new Date ( ) . getTime ( ) ;
714
743
try {
715
- // Then we try to move content file back on its place, if its still free
716
- // If we fail here, it means that some other process or thread has created content file
717
- // And this is okay, we will end up with two content files, but both with original content, unlucky files will have `.tmp` extension
718
- await xfs . linkPromise ( tmpPath , contentFilePath ) ;
719
- await xfs . unlinkPromise ( tmpPath ) ;
744
+ await xfs . linkPromise ( contentFilePath , dstPath ) ;
720
745
} catch ( e ) {
746
+ if ( e && e . code && e . code == `EXDEV` ) {
747
+ nmMode . value = NodeModulesMode . HARDLINKS_LOCAL ;
748
+ await baseFs . copyFilePromise ( srcPath , dstPath ) ;
749
+ }
721
750
}
722
751
}
723
- await xfs . linkPromise ( contentFilePath , dstPath ) ;
724
- doesContentFileExist = true ;
725
- } catch ( e ) {
726
- doesContentFileExist = false ;
752
+ } else {
753
+ await baseFs . copyFilePromise ( srcPath , dstPath ) ;
727
754
}
728
-
729
- if ( ! doesContentFileExist ) {
730
- const content = await baseFs . readFilePromise ( srcPath ) ;
731
- await atomicFileWrite ( globalHardlinksStore , contentFilePath , content ) ;
732
- try {
733
- await xfs . linkPromise ( contentFilePath , dstPath ) ;
734
- } catch ( e ) {
735
- if ( e && e . code && e . code == `EXDEV` ) {
736
- nmMode . value = NodeModulesMode . HARDLINKS_LOCAL ;
737
- await baseFs . copyFilePromise ( srcPath , dstPath ) ;
738
- }
739
- }
755
+ const mode = entry . mode & 0o777 ;
756
+ // An optimization - files will have rw-r-r permissions (0o644) by default, we can skip chmod for them
757
+ if ( mode !== 0o644 ) {
758
+ await xfs . chmodPromise ( dstPath , mode ) ;
740
759
}
741
- } else {
742
- await baseFs . copyFilePromise ( srcPath , dstPath ) ;
743
- }
744
- const mode = srcMode & 0o777 ;
745
- // An optimization - files will have rw-r-r permissions (0o644) by default, we can skip chmod for them
746
- if ( mode !== 0o644 ) {
747
- await xfs . chmodPromise ( dstPath , mode ) ;
748
760
}
749
761
}
750
762
@@ -756,6 +768,7 @@ type DirEntry = {
756
768
kind : DirEntryKind . FILE ;
757
769
mode : number ;
758
770
digest ?: string ;
771
+ mtimeMs ?: number ;
759
772
} | {
760
773
kind : DirEntryKind . DIRECTORY ;
761
774
} | {
@@ -808,23 +821,33 @@ const copyPromise = async (dstDir: PortablePath, srcDir: PortablePath, {baseFs,
808
821
allEntries = new Map ( Object . entries ( JSON . parse ( await xfs . readFilePromise ( entriesJsonPath , `utf8` ) ) ) ) as Map < PortablePath , DirEntry > ;
809
822
} catch ( e ) {
810
823
allEntries = await getEntriesRecursive ( ) ;
811
- await atomicFileWrite ( globalHardlinksStore , entriesJsonPath , Buffer . from ( JSON . stringify ( Object . fromEntries ( allEntries ) ) ) ) ;
812
824
}
813
825
} else {
814
826
allEntries = await getEntriesRecursive ( ) ;
815
827
}
816
828
829
+ let mtimesChanged = false ;
817
830
for ( const [ relativePath , entry ] of allEntries ) {
818
831
const srcPath = ppath . join ( srcDir , relativePath ) ;
819
832
const dstPath = ppath . join ( dstDir , relativePath ) ;
820
833
if ( entry . kind === DirEntryKind . DIRECTORY ) {
821
834
await xfs . mkdirPromise ( dstPath , { recursive : true } ) ;
822
835
} else if ( entry . kind === DirEntryKind . FILE ) {
823
- await copyFilePromise ( { srcPath, dstPath, srcMode : entry . mode , digest : entry . digest , nmMode, baseFs, globalHardlinksStore} ) ;
836
+ const originalMtime = entry . mtimeMs ;
837
+ await copyFilePromise ( { srcPath, dstPath, entry, nmMode, baseFs, globalHardlinksStore} ) ;
838
+ if ( entry . mtimeMs !== originalMtime ) {
839
+ mtimesChanged = true ;
840
+ }
824
841
} else if ( entry . kind === DirEntryKind . SYMLINK ) {
825
842
await symlinkPromise ( ppath . resolve ( ppath . dirname ( dstPath ) , entry . symlinkTo ) , dstPath ) ;
826
843
}
827
844
}
845
+
846
+ if ( nmMode . value === NodeModulesMode . HARDLINKS_GLOBAL && globalHardlinksStore && mtimesChanged && packageChecksum ) {
847
+ const entriesJsonPath = ppath . join ( globalHardlinksStore , packageChecksum . substring ( 0 , 2 ) as Filename , `${ packageChecksum . substring ( 2 ) } .json` as Filename ) ;
848
+ await xfs . removePromise ( entriesJsonPath ) ;
849
+ await atomicFileWrite ( globalHardlinksStore , entriesJsonPath , Buffer . from ( JSON . stringify ( Object . fromEntries ( allEntries ) ) ) ) ;
850
+ }
828
851
} ;
829
852
830
853
/**
0 commit comments