Bug 1380416 - Part 1 - In development builds on macOS and Linux use hardlinks rather than symlinks for placing resources in the app bundle draft
authorAlex Gaynor <agaynor@mozilla.com>
Fri, 14 Jul 2017 16:10:02 -0400
changeset 609859 b7967ec647edef8524a3b494b316775df6443feb
parent 609768 d43779e278d2e4d3e21dba2fcb585a3bf4b1288e
child 609860 c27c588bf6c206b6cdcf442c1471b53321694029
child 609875 5392934d2828209404ae023d5103e95aa609358c
push id68712
push userbmo:agaynor@mozilla.com
push dateMon, 17 Jul 2017 16:19:38 +0000
bugs1380416
milestone56.0a1
Bug 1380416 - Part 1 - In development builds on macOS and Linux use hardlinks rather than symlinks for placing resources in the app bundle MozReview-Commit-ID: 6qfVK5us405
config/nsinstall.c
python/mozbuild/mozbuild/jar.py
python/mozbuild/mozpack/files.py
--- a/config/nsinstall.c
+++ b/config/nsinstall.c
@@ -83,17 +83,17 @@ mkdirs(char *path, mode_t mode)
     if (cp && cp != path) {
 	*cp = '\0';
 	if ((lstat(path, &sb) < 0 || !S_ISDIR(sb.st_mode)) &&
 	    mkdirs(path, mode) < 0) {
 	    return -1;
 	}
 	*cp = '/';
     }
-    
+
     res = mkdir(path, mode);
     if ((res != 0) && (errno == EEXIST))
       return 0;
     else
       return res;
 }
 
 static uid_t
@@ -402,45 +402,41 @@ main(int argc, char **argv)
 			xchdir(cwd);
 		    }
 
 		    len = strlen(linkname);
 		}
 		name = linkname;
 	    }
 
-	    /* Check for a pre-existing symlink with identical content. */
-	    if (exists && (!S_ISLNK(tosb.st_mode) ||
-						readlink(toname, buf, sizeof buf) != len ||
-						strncmp(buf, name, (unsigned int)len) != 0 || 
-			((stat(name, &fromsb) == 0) &&
-			 (fromsb.st_mtime > tosb.st_mtime) 
-			 ))) {
-		(void) (S_ISDIR(tosb.st_mode) ? rmdir : unlink)(toname);
-		exists = 0;
-	    }
-	    if (!exists && symlink(name, toname) < 0)
-		fail("cannot make symbolic link %s", toname);
+	    /* Check for a pre-existing hardlink pointing at the same location. */
+      if (exists && stat(name, &fromsb) == 0 &&
+          (fromsb.st_dev != tosb.st_dev || fromsb.st_ino != tosb.st_ino)) {
+        (void) (S_ISDIR(tosb.st_mode) ? rmdir : unlink)(toname);
+        exists = 0;
+      }
+	    if (!exists && link(name, toname) < 0)
+		    fail("cannot make hard link %s", toname);
 #ifdef HAVE_LCHOWN
 	    if ((owner || group) && lchown(toname, uid, gid) < 0)
 		fail("cannot change owner of %s", toname);
 #endif
 
 	    if (linkname) {
 		free(linkname);
 		linkname = 0;
 	    }
 	} else {
 	    /* Copy from name to toname, which might be the same file. */
       if( stat(name, &sb) == 0 && S_IFDIR & sb.st_mode )
       {
         /* then is directory: must explicitly create destination dir  */
         /*  and manually copy files over                              */
         copydir( name, todir, mode, group, owner, dotimes, uid, gid );
-      } 
+      }
       else
       {
         copyfile(name, toname, mode, group, owner, dotimes, uid, gid);
       }
     }
 
 	free(toname);
     }
--- a/python/mozbuild/mozbuild/jar.py
+++ b/python/mozbuild/mozbuild/jar.py
@@ -540,17 +540,17 @@ class JarMaker(object):
 
             # remove previous link or file
             try:
                 os.remove(out)
             except OSError, e:
                 if e.errno != errno.ENOENT:
                     raise
             if sys.platform != 'win32':
-                os.symlink(src, out)
+                os.link(src, out)
             else:
                 # On Win32, use ctypes to create a hardlink
                 rv = CreateHardLink(out, src, None)
                 if rv == 0:
                     raise WinError()
 
 
 def main(args=None):
--- a/python/mozbuild/mozpack/files.py
+++ b/python/mozbuild/mozpack/files.py
@@ -299,16 +299,17 @@ class ExecutableFile(File):
 
 class AbsoluteSymlinkFile(File):
     '''File class that is copied by symlinking (if available).
 
     This class only works if the target path is absolute.
     '''
 
     def __init__(self, path):
+        raise TypeError("Don't use me! Use HardlinkFile instead.")
         if not os.path.isabs(path):
             raise ValueError('Symlink target not absolute: %s' % path)
 
         File.__init__(self, path)
 
     def copy(self, dest, skip_if_older=True):
         assert isinstance(dest, basestring)
 
@@ -386,16 +387,69 @@ class AbsoluteSymlinkFile(File):
         except EnvironmentError:
             os.remove(temp_dest)
             raise
 
         os.rename(temp_dest, dest)
         return True
 
 
+class HardlinkFile(File):
+    '''File class that is copied by hard linking (if available)
+
+    Basically link AbsoluteSymlinkFile, but with hard links.
+    '''
+
+    def copy(self, dest, skip_if_older=True):
+        assert isinstance(dest, basestring)
+
+        if not hasattr(os, 'link'):
+            return super(HardlinkFile, self).copy(
+                dest, skip_if_older=skip_if_older
+            )
+
+        try:
+            path_st = os.stat(self.path)
+        except OSError as e:
+            if e.errno == errno.ENOENT:
+                raise ErrorMessage('Hard link target path does not exist: %s' % self.path)
+            else:
+                raise
+
+        st = None
+        try:
+            st = os.lstat(dest)
+        except OSError as e:
+            if e.errno != errno.ENOENT:
+                raise
+
+        if st:
+            # The dest already points to the right place.
+            if st.st_dev == path_st.st_dev and st.st_ino == path_st.st_ino:
+                return False
+            # The dest exists and it points to the wrong place
+            os.remove(dest)
+
+        # Now, either the dest used to exist and we just deleted it, or it never
+        # existed
+        try:
+            os.link(self.path, dest)
+        except OSError:
+            # If we can't hard link, fall back to copying
+            return super(HardlinkFile, self).copy(
+                dest, skip_if_older=skip_if_older
+            )
+        else:
+            return True
+
+# TODO: Hack hack hack - a simple way of using this everywhere without needing
+# to actually fix everything.
+AbsoluteSymlinkFile = HardlinkFile
+
+
 class ExistingFile(BaseFile):
     '''
     File class that represents a file that may exist but whose content comes
     from elsewhere.
 
     This purpose of this class is to account for files that are installed via
     external means. It is typically only used in manifests or in registries to
     account for files.