Imported Upstream version 3.2
[platform/upstream/ccache.git] / util.c
diff --git a/util.c b/util.c
index 3b472de..f453959 100644 (file)
--- a/util.c
+++ b/util.c
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2002 Andrew Tridgell
- * Copyright (C) 2009-2013 Joel Rosdahl
+ * Copyright (C) 2009-2014 Joel Rosdahl
  *
  * This program is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License as published by the Free
@@ -38,21 +38,24 @@ static FILE *logfile;
 static bool
 init_log(void)
 {
-       extern char *cache_logfile;
+       extern struct conf *conf;
 
        if (logfile) {
                return true;
        }
-       if (!cache_logfile) {
+       assert(conf);
+       if (str_eq(conf->log_file, "")) {
                return false;
        }
-       logfile = fopen(cache_logfile, "a");
+       logfile = fopen(conf->log_file, "a");
        if (logfile) {
+#ifndef _WIN32
                int fd = fileno(logfile);
                int flags = fcntl(fd, F_GETFD, 0);
                if (flags >= 0) {
                        fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
                }
+#endif
                return true;
        } else {
                return false;
@@ -60,45 +63,88 @@ init_log(void)
 }
 
 static void
-log_prefix(void)
+log_prefix(bool log_updated_time)
 {
 #ifdef HAVE_GETTIMEOFDAY
        char timestamp[100];
        struct timeval tv;
        struct tm *tm;
+       static char prefix[200];
 
-       gettimeofday(&tv, NULL);
+       if (log_updated_time) {
+               gettimeofday(&tv, NULL);
 #ifdef __MINGW64_VERSION_MAJOR
-       tm = _localtime32(&tv.tv_sec);
+               tm = localtime((time_t*)&tv.tv_sec);
 #else
-       tm = localtime(&tv.tv_sec);
+               tm = localtime(&tv.tv_sec);
 #endif
-       strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S", tm);
-       fprintf(logfile, "[%s.%06d %-5d] ", timestamp, (int)tv.tv_usec,
-               (int)getpid());
+               strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S", tm);
+               snprintf(prefix, sizeof(prefix),
+                        "[%s.%06d %-5d] ", timestamp, (int)tv.tv_usec, (int)getpid());
+       }
+       fputs(prefix, logfile);
 #else
        fprintf(logfile, "[%-5d] ", (int)getpid());
 #endif
 }
 
+static long
+path_max(const char *path)
+{
+#ifdef PATH_MAX
+       (void)path;
+       return PATH_MAX;
+#elif defined(MAXPATHLEN)
+       (void)path;
+       return MAXPATHLEN;
+#elif defined(_PC_PATH_MAX)
+       long maxlen = pathconf(path, _PC_PATH_MAX);
+       if (maxlen >= 4096) {
+               return maxlen;
+       } else {
+               return 4096;
+       }
+#endif
+}
+
+static void
+vlog(const char *format, va_list ap, bool log_updated_time)
+{
+       if (!init_log()) {
+               return;
+       }
+
+       log_prefix(log_updated_time);
+       vfprintf(logfile, format, ap);
+       fprintf(logfile, "\n");
+}
+
 /*
- * Write a message to the CCACHE_LOGFILE location (adding a newline).
+ * Write a message to the log file (adding a newline) and flush.
  */
 void
 cc_log(const char *format, ...)
 {
        va_list ap;
-
-       if (!init_log()) {
-               return;
+       va_start(ap, format);
+       vlog(format, ap, true);
+       va_end(ap);
+       if (logfile) {
+               fflush(logfile);
        }
+}
 
-       log_prefix();
+/*
+ * Write a message to the log file (adding a newline) without flushing and with
+ * a reused timestamp.
+ */
+void
+cc_bulklog(const char *format, ...)
+{
+       va_list ap;
        va_start(ap, format);
-       vfprintf(logfile, format, ap);
+       vlog(format, ap, false);
        va_end(ap);
-       fprintf(logfile, "\n");
-       fflush(logfile);
 }
 
 /*
@@ -111,7 +157,7 @@ cc_log_argv(const char *prefix, char **argv)
                return;
        }
 
-       log_prefix();
+       log_prefix(true);
        fputs(prefix, logfile);
        print_command(logfile, argv);
        fflush(logfile);
@@ -129,7 +175,7 @@ fatal(const char *format, ...)
        va_end(ap);
 
        cc_log("FATAL: %s", msg);
-       fprintf(stderr, "ccache: FATAL: %s\n", msg);
+       fprintf(stderr, "ccache: error: %s\n", msg);
 
        exit(1);
 }
@@ -178,13 +224,13 @@ mkstemp(char *template)
 #endif
 
 /*
- * Copy src to dest, decompressing src if needed. compress_dest decides whether
- * dest will be compressed.
+ * Copy src to dest, decompressing src if needed. compress_level > 0 decides
+ * whether dest will be compressed, and with which compression level.
  */
 int
-copy_file(const char *src, const char *dest, int compress_dest)
+copy_file(const char *src, const char *dest, int compress_level)
 {
-       int fd_in = -1, fd_out = -1;
+       int fd_in, fd_out;
        gzFile gz_in = NULL, gz_out = NULL;
        char buf[10240];
        int n, written;
@@ -195,32 +241,27 @@ copy_file(const char *src, const char *dest, int compress_dest)
        struct stat st;
        int errnum;
 
-       tmp_name = format("%s.%s.XXXXXX", dest, tmp_string());
-       cc_log("Copying %s to %s via %s (%s)",
-              src, dest, tmp_name, compress_dest ? "compressed": "uncompressed");
+       /* open destination file */
+       tmp_name = x_strdup(dest);
+       fd_out = create_tmp_fd(&tmp_name);
+       cc_log("Copying %s to %s via %s (%scompressed)",
+              src, dest, tmp_name, compress_level > 0 ? "" : "un");
 
        /* open source file */
        fd_in = open(src, O_RDONLY | O_BINARY);
        if (fd_in == -1) {
                cc_log("open error: %s", strerror(errno));
-               return -1;
+               goto error;
        }
 
        gz_in = gzdopen(fd_in, "rb");
        if (!gz_in) {
                cc_log("gzdopen(src) error: %s", strerror(errno));
                close(fd_in);
-               return -1;
-       }
-
-       /* open destination file */
-       fd_out = mkstemp(tmp_name);
-       if (fd_out == -1) {
-               cc_log("mkstemp error: %s", strerror(errno));
                goto error;
        }
 
-       if (compress_dest) {
+       if (compress_level > 0) {
                /*
                 * A gzip file occupies at least 20 bytes, so it will always
                 * occupy an entire filesystem block, even for empty files.
@@ -231,20 +272,21 @@ copy_file(const char *src, const char *dest, int compress_dest)
                        goto error;
                }
                if (file_size(&st) == 0) {
-                       compress_dest = 0;
+                       compress_level = 0;
                }
        }
 
-       if (compress_dest) {
+       if (compress_level > 0) {
                gz_out = gzdopen(dup(fd_out), "wb");
                if (!gz_out) {
                        cc_log("gzdopen(dest) error: %s", strerror(errno));
                        goto error;
                }
+               gzsetparams(gz_out, compress_level, Z_DEFAULT_STRATEGY);
        }
 
        while ((n = gzread(gz_in, buf, sizeof(buf))) > 0) {
-               if (compress_dest) {
+               if (compress_level > 0) {
                        written = gzwrite(gz_out, buf, n);
                } else {
                        ssize_t count;
@@ -258,7 +300,7 @@ copy_file(const char *src, const char *dest, int compress_dest)
                        } while (written < n);
                }
                if (written != n) {
-                       if (compress_dest) {
+                       if (compress_level > 0) {
                                cc_log("gzwrite error: %s (errno: %s)",
                                       gzerror(gz_in, &errnum),
                                       strerror(errno));
@@ -333,11 +375,11 @@ error:
 
 /* Run copy_file() and, if successful, delete the source file. */
 int
-move_file(const char *src, const char *dest, int compress_dest)
+move_file(const char *src, const char *dest, int compress_level)
 {
        int ret;
 
-       ret = copy_file(src, dest, compress_dest);
+       ret = copy_file(src, dest, compress_level);
        if (ret != -1) {
                x_unlink(src);
        }
@@ -349,10 +391,10 @@ move_file(const char *src, const char *dest, int compress_dest)
  * are on the same file system.
  */
 int
-move_uncompressed_file(const char *src, const char *dest, int compress_dest)
+move_uncompressed_file(const char *src, const char *dest, int compress_level)
 {
-       if (compress_dest) {
-               return move_file(src, dest, compress_dest);
+       if (compress_level > 0) {
+               return move_file(src, dest, compress_level);
        } else {
                return x_rename(src, dest);
        }
@@ -398,6 +440,45 @@ create_dir(const char *dir)
        return 0;
 }
 
+/* Create directories leading to path. Returns 0 on success, otherwise -1. */
+int
+create_parent_dirs(const char *path)
+{
+       struct stat st;
+       int res;
+       char *parent = dirname(path);
+
+       if (stat(parent, &st) == 0) {
+               if (S_ISDIR(st.st_mode)) {
+                       res = 0;
+               } else {
+                       res = -1;
+                       errno = ENOTDIR;
+               }
+       } else {
+               res = create_parent_dirs(parent);
+               if (res == 0) {
+                       res = mkdir(parent, 0777);
+                       /* Have to handle the condition of the directory already existing because
+                        * the file system could have changed in between calling stat and
+                        * actually creating the directory. This can happen when there are
+                        * multiple instances of ccache running and trying to create the same
+                        * directory chain, which usually is the case when the cache root does
+                        * not initially exist. As long as one of the processes creates the
+                        * directories then our condition is satisfied and we avoid a race
+                        * condition.
+                        */
+                       if (res != 0 && errno == EEXIST) {
+                               res = 0;
+                       }
+               } else {
+                       res = -1;
+               }
+       }
+       free(parent);
+       return res;
+}
+
 /*
  * Return a static string with the current hostname.
  */
@@ -418,8 +499,8 @@ get_hostname(void)
 }
 
 /*
- * Return a string to be used to distinguish temporary files. Also tries to
- * cope with NFS by adding the local hostname.
+ * Return a string to be passed to mkstemp to create a temporary file. Also
+ * tries to cope with NFS by adding the local hostname.
  */
 const char *
 tmp_string(void)
@@ -427,15 +508,18 @@ tmp_string(void)
        static char *ret;
 
        if (!ret) {
-               ret = format("%s.%u", get_hostname(), (unsigned)getpid());
+               ret = format("%s.%u.XXXXXX", get_hostname(), (unsigned)getpid());
        }
 
        return ret;
 }
 
-/* Return the hash result as a hex string. Caller frees. */
+/*
+ * Return the hash result as a hex string. Size -1 means don't include size
+ * suffix. Caller frees.
+ */
 char *
-format_hash_as_string(const unsigned char *hash, unsigned size)
+format_hash_as_string(const unsigned char *hash, int size)
 {
        char *ret;
        int i;
@@ -444,7 +528,9 @@ format_hash_as_string(const unsigned char *hash, unsigned size)
        for (i = 0; i < 16; i++) {
                sprintf(&ret[i*2], "%02x", (unsigned) hash[i]);
        }
-       sprintf(&ret[i*2], "-%u", size);
+       if (size >= 0) {
+               sprintf(&ret[i*2], "-%u", size);
+       }
 
        return ret;
 }
@@ -469,12 +555,16 @@ create_cachedirtag(const char *dir)
                goto error;
        }
        f = fopen(filename, "w");
-       if (!f) goto error;
+       if (!f) {
+               goto error;
+       }
        if (fwrite(CACHEDIR_TAG, sizeof(CACHEDIR_TAG)-1, 1, f) != 1) {
                fclose(f);
                goto error;
        }
-       if (fclose(f)) goto error;
+       if (fclose(f)) {
+               goto error;
+       }
 success:
        free(filename);
        return 0;
@@ -496,7 +586,9 @@ format(const char *format, ...)
        }
        va_end(ap);
 
-       if (!*ptr) fatal("Internal error in format");
+       if (!*ptr) {
+               fatal("Internal error in format");
+       }
        return ptr;
 }
 
@@ -591,7 +683,9 @@ void *
 x_realloc(void *ptr, size_t size)
 {
        void *p2;
-       if (!ptr) return x_malloc(size);
+       if (!ptr) {
+               return x_malloc(size);
+       }
        p2 = realloc(ptr, size);
        if (!p2) {
                fatal("x_realloc: Could not allocate %lu bytes", (unsigned long)size);
@@ -599,12 +693,22 @@ x_realloc(void *ptr, size_t size)
        return p2;
 }
 
+/* This is like unsetenv. */
+void x_unsetenv(const char *name)
+{
+#ifdef HAVE_UNSETENV
+       unsetenv(name);
+#else
+       putenv(x_strdup(name)); /* Leak to environment. */
+#endif
+}
 
 /*
- * This is like x_asprintf() but frees *ptr if *ptr != NULL.
+ * Construct a string according to the format and store it in *ptr. The
+ * original *ptr is then freed.
  */
 void
-x_asprintf2(char **ptr, const char *format, ...)
+reformat(char **ptr, const char *format, ...)
 {
        char *saved = *ptr;
        va_list ap;
@@ -612,11 +716,13 @@ x_asprintf2(char **ptr, const char *format, ...)
        *ptr = NULL;
        va_start(ap, format);
        if (vasprintf(ptr, format, ap) == -1) {
-               fatal("Out of memory in x_asprintf2");
+               fatal("Out of memory in reformat");
        }
        va_end(ap);
 
-       if (!ptr) fatal("Out of memory in x_asprintf2");
+       if (!ptr) {
+               fatal("Out of memory in reformat");
+       }
        if (saved) {
                free(saved);
        }
@@ -632,16 +738,24 @@ traverse(const char *dir, void (*fn)(const char *, struct stat *))
        struct dirent *de;
 
        d = opendir(dir);
-       if (!d) return;
+       if (!d) {
+               return;
+       }
 
        while ((de = readdir(d))) {
                char *fname;
                struct stat st;
 
-               if (str_eq(de->d_name, ".")) continue;
-               if (str_eq(de->d_name, "..")) continue;
+               if (str_eq(de->d_name, ".")) {
+                       continue;
+               }
+               if (str_eq(de->d_name, "..")) {
+                       continue;
+               }
 
-               if (strlen(de->d_name) == 0) continue;
+               if (strlen(de->d_name) == 0) {
+                       continue;
+               }
 
                fname = format("%s/%s", dir, de->d_name);
                if (lstat(fname, &st)) {
@@ -666,39 +780,49 @@ traverse(const char *dir, void (*fn)(const char *, struct stat *))
 
 /* return the base name of a file - caller frees */
 char *
-basename(const char *s)
+basename(const char *path)
 {
        char *p;
-       p = strrchr(s, '/');
-       if (p) s = p + 1;
+       p = strrchr(path, '/');
+       if (p) {
+               path = p + 1;
+       }
 #ifdef _WIN32
-       p = strrchr(s, '\\');
-       if (p) s = p + 1;
+       p = strrchr(path, '\\');
+       if (p) {
+               path = p + 1;
+       }
 #endif
 
-       return x_strdup(s);
+       return x_strdup(path);
 }
 
 /* return the dir name of a file - caller frees */
 char *
-dirname(char *s)
+dirname(const char *path)
 {
        char *p;
-       char *p2 = NULL;
-       s = x_strdup(s);
+#ifdef _WIN32
+       char *p2;
+#endif
+       char *s;
+       s = x_strdup(path);
        p = strrchr(s, '/');
 #ifdef _WIN32
        p2 = strrchr(s, '\\');
-#endif
-       if (p < p2)
+       if (!p || (p2 && p < p2)) {
                p = p2;
-       if (p) {
-               *p = 0;
-               return s;
-       } else {
+       }
+#endif
+       if (!p) {
                free(s);
-               return x_strdup(".");
+               s = x_strdup(".");
+       } else if (p == s) {
+               *(p + 1) = 0;
+       } else {
+               *p = 0;
        }
+       return s;
 }
 
 /*
@@ -749,82 +873,86 @@ file_size(struct stat *st)
 #endif
 }
 
-/* a safe open/create for read-write */
-int
-safe_open(const char *fname)
+/* Format a size as a human-readable string. Caller frees. */
+char *
+format_human_readable_size(uint64_t v)
 {
-       int fd = open(fname, O_RDWR|O_BINARY);
-       if (fd == -1 && errno == ENOENT) {
-               fd = open(fname, O_RDWR|O_CREAT|O_EXCL|O_BINARY, 0666);
-               if (fd == -1 && errno == EEXIST) {
-                       fd = open(fname, O_RDWR|O_BINARY);
-               }
+       char *s;
+       if (v >= 1000*1000*1000) {
+               s = format("%.1f GB", v/((double)(1000*1000*1000)));
+       } else if (v >= 1000*1000) {
+               s = format("%.1f MB", v/((double)(1000*1000)));
+       } else {
+               s = format("%.1f kB", v/((double)(1000)));
        }
-       return fd;
+       return s;
 }
 
-/* Format a size (in KiB) as a human-readable string. Caller frees. */
+/* Format a size as a parsable string. Caller frees. */
 char *
-format_size(size_t v)
+format_parsable_size_with_suffix(uint64_t size)
 {
        char *s;
-       if (v >= 1024*1024) {
-               s = format("%.1f Gbytes", v/((double)(1024*1024)));
-       } else if (v >= 1024) {
-               s = format("%.1f Mbytes", v/((double)(1024)));
+       if (size >= 1000*1000*1000) {
+               s = format("%.1fG", size / ((double)(1000*1000*1000)));
+       } else if (size >= 1000*1000) {
+               s = format("%.1fM", size / ((double)(1000*1000)));
+       } else if (size >= 1000) {
+               s = format("%.1fk", size / ((double)(1000)));
        } else {
-               s = format("%.0f Kbytes", (double)v);
+               s = format("%u", (unsigned)size);
        }
        return s;
 }
 
-/* return a value in multiples of 1024 give a string that can end
-   in K, M or G
-*/
-size_t
-value_units(const char *s)
+/*
+ * Parse a "size value", i.e. a string that can end in k, M, G, T (10-based
+ * suffixes) or Ki, Mi, Gi, Ti (2-based suffixes). For backward compatibility,
+ * K is also recognized as a synonym of k.
+ */
+bool
+parse_size_with_suffix(const char *str, uint64_t *size)
 {
-       char m;
-       double v = atof(s);
-       m = s[strlen(s)-1];
-       switch (m) {
-       case 'G':
-       case 'g':
-       default:
-               v *= 1024*1024;
-               break;
-       case 'M':
-       case 'm':
-               v *= 1024;
-               break;
-       case 'K':
-       case 'k':
-               v *= 1;
-               break;
-       }
-       return (size_t)v;
-}
+       char *p;
+       double x;
 
-#ifndef _WIN32
-static long
-path_max(const char *path)
-{
-#ifdef PATH_MAX
-       (void)path;
-       return PATH_MAX;
-#elif defined(MAXPATHLEN)
-       (void)path;
-       return MAXPATHLEN;
-#elif defined(_PC_PATH_MAX)
-       long maxlen = pathconf(path, _PC_PATH_MAX);
-       if (maxlen >= 4096) {
-               return maxlen;
-       } else {
-               return 4096;
+       errno = 0;
+       x = strtod(str, &p);
+       if (errno != 0 || x < 0 || p == str || *str == '\0') {
+               return false;
        }
-#endif
+
+       while (isspace(*p)) {
+               ++p;
+       }
+
+       if (*p != '\0') {
+               unsigned multiplier;
+               if (*(p+1) == 'i') {
+                       multiplier = 1024;
+               } else {
+                       multiplier = 1000;
+               }
+               switch (*p) {
+               case 'T':
+                       x *= multiplier;
+               case 'G':
+                       x *= multiplier;
+               case 'M':
+                       x *= multiplier;
+               case 'K':
+               case 'k':
+                       x *= multiplier;
+                       break;
+               default:
+                       return false;
+               }
+       }
+       *size = x;
+       return true;
 }
 
+
 /*
   a sane realpath() function, trying to cope with stupid path limits and
   a broken API
@@ -834,11 +962,21 @@ x_realpath(const char *path)
 {
        long maxlen = path_max(path);
        char *ret, *p;
+#ifdef _WIN32
+       HANDLE path_handle;
+#endif
 
        ret = x_malloc(maxlen);
 
 #if HAVE_REALPATH
        p = realpath(path, ret);
+#elif defined(_WIN32)
+       path_handle = CreateFile(
+               path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
+               FILE_ATTRIBUTE_NORMAL, NULL);
+       GetFinalPathNameByHandle(path_handle, ret, maxlen, FILE_NAME_NORMALIZED);
+       CloseHandle(path_handle);
+       p = ret+4;// strip the \\?\ from the file name
 #else
        /* yes, there are such systems. This replacement relies on
           the fact that when we call x_realpath we only care about symlinks */
@@ -860,7 +998,6 @@ x_realpath(const char *path)
        free(ret);
        return NULL;
 }
-#endif /* !_WIN32 */
 
 /* a getcwd that will returns an allocated buffer */
 char *
@@ -868,7 +1005,7 @@ gnu_getcwd(void)
 {
        unsigned size = 128;
 
-       while (1) {
+       while (true) {
                char *buffer = (char *)x_malloc(size);
                if (getcwd(buffer, size) == buffer) {
                        return buffer;
@@ -882,18 +1019,75 @@ gnu_getcwd(void)
        }
 }
 
-/* create an empty file */
-int
-create_empty_file(const char *fname)
+#ifndef HAVE_STRTOK_R
+/* strtok_r replacement */
+char *
+strtok_r(char *str, const char *delim, char **saveptr)
 {
-       int fd;
+       int len;
+       char *ret;
+       if (!str)
+               str = *saveptr;
+       len = strlen(str);
+       ret = strtok(str, delim);
+       if (ret) {
+               char *save = ret;
+               while (*save++);
+               if ((len + 1) == (intptr_t) (save - str))
+                       save--;
+               *saveptr = save;
+       }
+       return ret;
+}
+#endif
 
-       fd = open(fname, O_WRONLY|O_CREAT|O_TRUNC|O_EXCL|O_BINARY, 0666);
+/*
+ * Create an empty temporary file. *fname will be reallocated and set to the
+ * resulting filename. Returns an open file descriptor to the file.
+ */
+int
+create_tmp_fd(char **fname)
+{
+       char *template = format("%s.%s", *fname, tmp_string());
+       int fd = mkstemp(template);
+       if (fd == -1 && errno == ENOENT) {
+               if (create_parent_dirs(template) != 0) {
+                       fatal("Failed to create directory %s: %s",
+                             dirname(template), strerror(errno));
+               }
+               reformat(&template, "%s.%s", *fname, tmp_string());
+               fd = mkstemp(template);
+       }
        if (fd == -1) {
-               return -1;
+               fatal("Failed to create file %s: %s", template, strerror(errno));
        }
-       close(fd);
-       return 0;
+       free(*fname);
+       *fname = template;
+       return fd;
+}
+
+/*
+ * Create an empty temporary file. *fname will be reallocated and set to the
+ * resulting filename. Returns an open FILE*.
+ */
+FILE *
+create_tmp_file(char **fname, const char *mode)
+{
+       FILE *file = fdopen(create_tmp_fd(fname), mode);
+       if (!file) {
+               fatal("Failed to create file %s: %s", *fname, strerror(errno));
+       }
+       return file;
+}
+
+/*
+ * Create an empty temporary file. *fname will be reallocated and set to the
+ * resulting filename.
+ */
+void
+create_empty_tmp_file(char **fname)
+{
+       close(create_tmp_fd(fname));
 }
 
 /*
@@ -906,6 +1100,12 @@ get_home_directory(void)
        if (p) {
                return p;
        }
+#ifdef _WIN32
+       p = getenv("APPDATA");
+       if (p) {
+               return p;
+       }
+#endif
 #ifdef HAVE_GETPWUID
        {
                struct passwd *pwd = getpwuid(getuid());
@@ -984,24 +1184,14 @@ common_dir_prefix_length(const char *s1, const char *s2)
                ++p1;
                ++p2;
        }
-       if (*p2 == '/') {
-               /* s2 starts with "s1/". */
-               return p1 - s1;
-       }
-       if (!*p2) {
-               /* s2 is equal to s1. */
-               if (p2 == s2 + 1) {
-                       /* Special case for s1 and s2 both being "/". */
-                       return 0;
-               } else {
-                       return p1 - s1;
-               }
-       }
-       /* Compute the common directory prefix */
-       while (p1 > s1 && *p1 != '/') {
+       while ((*p1 && *p1 != '/') || (*p2 && *p2 != '/')) {
                p1--;
                p2--;
        }
+       if (!*p1 && !*p2 && p2 == s2 + 1) {
+               /* Special case for s1 and s2 both being "/". */
+               return 0;
+       }
        return p1 - s1;
 }
 
@@ -1030,12 +1220,12 @@ get_relative_path(const char *from, const char *to)
        if (common_prefix_len > 0 || !str_eq(from, "/")) {
                for (p = from + common_prefix_len; *p; p++) {
                        if (*p == '/') {
-                               x_asprintf2(&result, "../%s", result);
+                               reformat(&result, "../%s", result);
                        }
                }
        }
        if (strlen(to) > common_prefix_len) {
-               x_asprintf2(&result, "%s%s", result, to + common_prefix_len + 1);
+               reformat(&result, "%s%s", result, to + common_prefix_len + 1);
        }
        i = strlen(result) - 1;
        while (i >= 0 && result[i] == '/') {
@@ -1069,12 +1259,12 @@ bool
 is_full_path(const char *path)
 {
        if (strchr(path, '/'))
-               return 1;
+               return true;
 #ifdef _WIN32
        if (strchr(path, '\\'))
-               return 1;
+               return true;
 #endif
-       return 0;
+       return false;
 }
 
 /*
@@ -1111,7 +1301,7 @@ x_rename(const char *oldpath, const char *newpath)
 int
 tmp_unlink(const char *path)
 {
-       cc_log("Unlink %s (as-tmp)", path);
+       cc_log("Unlink %s", path);
        return unlink(path);
 }
 
@@ -1126,7 +1316,7 @@ x_unlink(const char *path)
         * file. We don't care if the temp file is trashed, so it's always safe to
         * unlink it first.
         */
-       char *tmp_name = format("%s.tmp.rm.%s", path, tmp_string());
+       char *tmp_name = format("%s.rm.%s", path, tmp_string());
        int result = 0;
        cc_log("Unlink %s via %s", path, tmp_name);
        if (x_rename(path, tmp_name) == -1) {
@@ -1134,7 +1324,10 @@ x_unlink(const char *path)
                goto out;
        }
        if (unlink(tmp_name) == -1) {
-               result = -1;
+               /* If it was released in a race, that's OK. */
+               if (errno != ENOENT) {
+                       result = -1;
+               }
        }
 out:
        free(tmp_name);
@@ -1149,14 +1342,6 @@ x_readlink(const char *path)
        long maxlen = path_max(path);
        ssize_t len;
        char *buf;
-#ifdef PATH_MAX
-       maxlen = PATH_MAX;
-#elif defined(MAXPATHLEN)
-       maxlen = MAXPATHLEN;
-#elif defined(_PC_PATH_MAX)
-       maxlen = pathconf(path, _PC_PATH_MAX);
-#endif
-       if (maxlen < 4096) maxlen = 4096;
 
        buf = x_malloc(maxlen);
        len = readlink(path, buf, maxlen-1);
@@ -1187,13 +1372,12 @@ read_file(const char *path, size_t size_hint, char **data, size_t *size)
        }
        size_hint = (size_hint < 1024) ? 1024 : size_hint;
 
-       fd = open(path, O_RDONLY);
+       fd = open(path, O_RDONLY | O_BINARY);
        if (fd == -1) {
                return false;
        }
        allocated = size_hint;
        *data = x_malloc(allocated);
-       ret = 0;
        while (true) {
                if (pos > allocated / 2) {
                        allocated *= 2;
@@ -1221,15 +1405,15 @@ read_file(const char *path, size_t size_hint, char **data, size_t *size)
 
 /*
  * Return the content (with NUL termination) of a text file, or NULL on error.
- * Caller frees.
+ * Caller frees. Size hint 0 means no hint.
  */
 char *
-read_text_file(const char *path)
+read_text_file(const char *path, size_t size_hint)
 {
        size_t size;
        char *data;
 
-       if (read_file(path, 0, &data, &size)) {
+       if (read_file(path, size_hint, &data, &size)) {
                data = x_realloc(data, size + 1);
                data[size] = '\0';
                return data;
@@ -1237,3 +1421,85 @@ read_text_file(const char *path)
                return NULL;
        }
 }
+
+static bool
+expand_variable(const char **str, char **result, char **errmsg)
+{
+       bool curly;
+       const char *p, *q;
+       char *name;
+       const char *value;
+
+       assert(**str == '$');
+       p = *str + 1;
+       if (*p == '{') {
+               curly = true;
+               ++p;
+       } else {
+               curly = false;
+       }
+       q = p;
+       while (isalnum(*q) || *q == '_') {
+               ++q;
+       }
+       if (curly) {
+               if (*q != '}') {
+                       *errmsg = format("syntax error: missing '}' after \"%s\"", p);
+                       return NULL;
+               }
+       }
+
+       if (q == p) {
+               /* Special case: don't consider a single $ the start of a variable. */
+               reformat(result, "%s$", *result);
+               return true;
+       }
+
+       name = x_strndup(p, q - p);
+       value = getenv(name);
+       if (!value) {
+               *errmsg = format("environment variable \"%s\" not set", name);
+               free(name);
+               return false;
+       }
+       reformat(result, "%s%s", *result, value);
+       if (!curly) {
+               --q;
+       }
+       *str = q;
+       free(name);
+       return true;
+}
+
+/*
+ * Substitute all instances of $VAR or ${VAR}, where VAR is an environment
+ * variable, in a string. Caller frees. If one of the environment variables
+ * doesn't exist, NULL will be returned and *errmsg will be an appropriate
+ * error message (caller frees).
+ */
+char *
+subst_env_in_string(const char *str, char **errmsg)
+{
+       const char *p; /* Interval start. */
+       const char *q; /* Interval end. */
+       char *result;
+
+       assert(errmsg);
+       *errmsg = NULL;
+
+       result = x_strdup("");
+       p = str;
+       q = str;
+       for (q = str; *q; ++q) {
+               if (*q == '$') {
+                       reformat(&result, "%s%.*s", result, (int)(q - p), p);
+                       if (!expand_variable(&q, &result, errmsg)) {
+                               free(result);
+                               return NULL;
+                       }
+                       p = q + 1;
+               }
+       }
+       reformat(&result, "%s%.*s", result, (int)(q - p), p);
+       return result;
+}