| // Copyright 2014 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| // This program creates a "runfiles tree" from a "runfiles manifest". |
| // |
| // The command line arguments are an input manifest INPUT and an output |
| // directory RUNFILES. First, the files in the RUNFILES directory are scanned |
| // and any extraneous ones are removed. Second, any missing files are created. |
| // Finally, a copy of the input manifest is written to RUNFILES/MANIFEST. |
| // |
| // The input manifest consists of lines, each containing a relative path within |
| // the runfiles, a space, and an optional absolute path. If this second path |
| // is present, a symlink is created pointing to it; otherwise an empty file is |
| // created. |
| // |
| // Given the line |
| // <workspace root>/output/path /real/path |
| // we will create directories |
| // RUNFILES/<workspace root> |
| // RUNFILES/<workspace root>/output |
| // a symlink |
| // RUNFILES/<workspace root>/output/path -> /real/path |
| // and the output manifest will contain a line |
| // <workspace root>/output/path /real/path |
| // |
| // If --use_metadata is supplied, every other line is treated as opaque |
| // metadata, and is ignored here. |
| // |
| // All output paths must be relative and generally (but not always) begin with |
| // <workspace root>. No output path may be equal to another. No output path may |
| // be a path prefix of another. |
| |
| #define _FILE_OFFSET_BITS 64 |
| |
| #include <dirent.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <limits.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <stdlib.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| |
| #include <map> |
| #include <string> |
| |
| // program_invocation_short_name is not portable. |
| static const char *argv0; |
| |
| const char *input_filename; |
| const char *output_base_dir; |
| |
| #define LOG() { \ |
| fprintf(stderr, "%s (args %s %s): ", \ |
| argv0, input_filename, output_base_dir); \ |
| } |
| |
| #define DIE(args...) { \ |
| LOG(); \ |
| fprintf(stderr, args); \ |
| fprintf(stderr, "\n"); \ |
| exit(1); \ |
| } |
| |
| #define PDIE(args...) { \ |
| int saved_errno = errno; \ |
| LOG(); \ |
| fprintf(stderr, args); \ |
| fprintf(stderr, ": %s [%d]\n", strerror(saved_errno), saved_errno); \ |
| exit(1); \ |
| } |
| |
| enum FileType { |
| FILE_TYPE_REGULAR, |
| FILE_TYPE_DIRECTORY, |
| FILE_TYPE_SYMLINK |
| }; |
| |
| struct FileInfo { |
| FileType type; |
| std::string symlink_target; |
| |
| bool operator==(const FileInfo &other) const { |
| return type == other.type && symlink_target == other.symlink_target; |
| } |
| |
| bool operator!=(const FileInfo &other) const { |
| return !(*this == other); |
| } |
| }; |
| |
| typedef std::map<std::string, FileInfo> FileInfoMap; |
| |
| // Replaces \s, \n, and \b with their respective characters. |
| std::string Unescape(const std::string &path) { |
| std::string result; |
| result.reserve(path.size()); |
| for (size_t i = 0; i < path.size(); ++i) { |
| if (path[i] == '\\' && i + 1 < path.size()) { |
| switch (path[i + 1]) { |
| case 's': { |
| result.push_back(' '); |
| break; |
| } |
| case 'n': { |
| result.push_back('\n'); |
| break; |
| } |
| case 'b': { |
| result.push_back('\\'); |
| break; |
| } |
| default: { |
| result.push_back(path[i]); |
| result.push_back(path[i + 1]); |
| break; |
| } |
| } |
| ++i; |
| } else { |
| result.push_back(path[i]); |
| } |
| } |
| return result; |
| } |
| |
| class RunfilesCreator { |
| public: |
| explicit RunfilesCreator(const std::string &output_base) |
| : output_base_(output_base), |
| output_filename_("MANIFEST"), |
| temp_filename_(output_filename_ + ".tmp") { |
| SetupOutputBase(); |
| if (chdir(output_base_.c_str()) != 0) { |
| PDIE("chdir '%s'", output_base_.c_str()); |
| } |
| } |
| |
| void ReadManifest(const std::string &manifest_file, bool allow_relative, |
| bool use_metadata) { |
| // Remove file left over from previous invocation. This ensures that |
| // opening succeeds if the existing file is read-only. |
| if (unlink(temp_filename_.c_str()) != 0 && errno != ENOENT) { |
| PDIE("removing temporary file at '%s/%s'", output_base_.c_str(), |
| temp_filename_.c_str()); |
| } |
| FILE *outfile = fopen(temp_filename_.c_str(), "w"); |
| if (!outfile) { |
| PDIE("opening '%s/%s' for writing", output_base_.c_str(), |
| temp_filename_.c_str()); |
| } |
| FILE *infile = fopen(manifest_file.c_str(), "r"); |
| if (!infile) { |
| PDIE("opening '%s' for reading", manifest_file.c_str()); |
| } |
| |
| // read input manifest |
| int lineno = 0; |
| char buf[3 * PATH_MAX]; |
| while (fgets(buf, sizeof buf, infile)) { |
| // copy line to output manifest |
| if (fputs(buf, outfile) == EOF) { |
| PDIE("writing to '%s/%s'", output_base_.c_str(), |
| temp_filename_.c_str()); |
| } |
| |
| // parse line |
| ++lineno; |
| // Skip metadata lines. They are used solely for |
| // dependency checking. |
| if (use_metadata && lineno % 2 == 0) continue; |
| |
| int n = strlen(buf)-1; |
| if (!n || buf[n] != '\n') { |
| DIE("missing terminator at line %d: '%s'\n", lineno, buf); |
| } |
| buf[n] = '\0'; |
| if (buf[0] == '/') { |
| DIE("paths must not be absolute: line %d: '%s'\n", lineno, buf); |
| } |
| std::string link; |
| std::string target; |
| if (buf[0] == ' ') { |
| // The link path contains escape sequences for spaces and backslashes. |
| char *s = strchr(buf + 1, ' '); |
| if (!s) { |
| DIE("missing field delimiter at line %d: '%s'\n", lineno, buf); |
| } |
| link = Unescape(std::string(buf + 1, s)); |
| target = Unescape(s + 1); |
| } else { |
| // The line is of the form "foo /target/path", with only a single space |
| // in the link path. |
| const char *s = strchr(buf, ' '); |
| if (!s) { |
| DIE("missing field delimiter at line %d: '%s'\n", lineno, buf); |
| } |
| link = std::string(buf, s - buf); |
| target = s + 1; |
| } |
| if (!allow_relative && target[0] != '\0' && target[0] != '/' |
| && target[1] != ':') { // Match Windows paths, e.g. C:\foo or C:/foo. |
| DIE("expected absolute path at line %d: '%s'\n", lineno, buf); |
| } |
| |
| FileInfo *info = &manifest_[link]; |
| if (target[0] == '\0') { |
| // No target means an empty file. |
| info->type = FILE_TYPE_REGULAR; |
| } else { |
| info->type = FILE_TYPE_SYMLINK; |
| info->symlink_target = target; |
| } |
| |
| FileInfo parent_info; |
| parent_info.type = FILE_TYPE_DIRECTORY; |
| |
| while (true) { |
| int k = link.rfind('/'); |
| if (k < 0) break; |
| link.erase(k, std::string::npos); |
| if (!manifest_.insert(std::make_pair(link, parent_info)).second) break; |
| } |
| } |
| if (fclose(outfile) != 0) { |
| PDIE("writing to '%s/%s'", output_base_.c_str(), |
| temp_filename_.c_str()); |
| } |
| fclose(infile); |
| |
| // Don't delete the temp manifest file. |
| manifest_[temp_filename_].type = FILE_TYPE_REGULAR; |
| } |
| |
| void CreateRunfiles() { |
| if (unlink(output_filename_.c_str()) != 0 && errno != ENOENT) { |
| PDIE("removing previous file at '%s/%s'", output_base_.c_str(), |
| output_filename_.c_str()); |
| } |
| |
| ScanTreeAndPrune("."); |
| CreateFiles(); |
| |
| // rename output file into place |
| if (rename(temp_filename_.c_str(), output_filename_.c_str()) != 0) { |
| PDIE("renaming '%s/%s' to '%s/%s'", |
| output_base_.c_str(), temp_filename_.c_str(), |
| output_base_.c_str(), output_filename_.c_str()); |
| } |
| } |
| |
| private: |
| void SetupOutputBase() { |
| struct stat st; |
| if (stat(output_base_.c_str(), &st) != 0) { |
| // Technically, this will cause problems if the user's umask contains |
| // 0200, but we don't care. Anyone who does that deserves what's coming. |
| if (mkdir(output_base_.c_str(), 0777) != 0) { |
| PDIE("creating directory '%s'", output_base_.c_str()); |
| } |
| } else { |
| EnsureDirReadAndWritePerms(output_base_); |
| } |
| } |
| |
| void ScanTreeAndPrune(const std::string &path) { |
| // A note on non-empty files: |
| // We don't distinguish between empty and non-empty files. That is, if |
| // there's a file that has contents, we don't truncate it here, even though |
| // the manifest supports creation of empty files, only. Given that |
| // .runfiles are *supposed* to be immutable, this shouldn't be a problem. |
| EnsureDirReadAndWritePerms(path); |
| |
| struct dirent *entry; |
| DIR *dh = opendir(path.c_str()); |
| if (!dh) { |
| PDIE("opendir '%s'", path.c_str()); |
| } |
| |
| errno = 0; |
| const std::string prefix = (path == "." ? "" : path + "/"); |
| while ((entry = readdir(dh)) != nullptr) { |
| if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue; |
| |
| std::string entry_path = prefix + entry->d_name; |
| FileInfo actual_info; |
| actual_info.type = DentryToFileType(entry_path, entry); |
| |
| if (actual_info.type == FILE_TYPE_SYMLINK) { |
| ReadLinkOrDie(entry_path, &actual_info.symlink_target); |
| } |
| |
| FileInfoMap::iterator expected_it = manifest_.find(entry_path); |
| if (expected_it == manifest_.end() || |
| expected_it->second != actual_info) { |
| #if !defined(__CYGWIN__) |
| DelTree(entry_path, actual_info.type); |
| #else |
| // On Windows, if deleting failed, lamely assume that |
| // the link points to the right place. |
| if (!DelTree(entry_path, actual_info.type)) { |
| manifest_.erase(expected_it); |
| } |
| #endif |
| } else { |
| manifest_.erase(expected_it); |
| if (actual_info.type == FILE_TYPE_DIRECTORY) { |
| ScanTreeAndPrune(entry_path); |
| } |
| } |
| |
| errno = 0; |
| } |
| if (errno != 0) { |
| PDIE("reading directory '%s'", path.c_str()); |
| } |
| closedir(dh); |
| } |
| |
| void CreateFiles() { |
| for (FileInfoMap::const_iterator it = manifest_.begin(); |
| it != manifest_.end(); ++it) { |
| const std::string &path = it->first; |
| switch (it->second.type) { |
| case FILE_TYPE_DIRECTORY: |
| if (mkdir(path.c_str(), 0777) != 0) { |
| PDIE("mkdir '%s'", path.c_str()); |
| } |
| break; |
| case FILE_TYPE_REGULAR: |
| { |
| int fd = open(path.c_str(), O_CREAT|O_EXCL|O_WRONLY, 0555); |
| if (fd < 0) { |
| PDIE("creating empty file '%s'", path.c_str()); |
| } |
| close(fd); |
| } |
| break; |
| case FILE_TYPE_SYMLINK: |
| { |
| const std::string& target = it->second.symlink_target; |
| if (symlink(target.c_str(), path.c_str()) != 0) { |
| PDIE("symlinking '%s' -> '%s'", path.c_str(), target.c_str()); |
| } |
| } |
| break; |
| } |
| } |
| } |
| |
| FileType DentryToFileType(const std::string &path, struct dirent *ent) { |
| #ifdef _DIRENT_HAVE_D_TYPE |
| if (ent->d_type != DT_UNKNOWN) { |
| if (ent->d_type == DT_DIR) { |
| return FILE_TYPE_DIRECTORY; |
| } else if (ent->d_type == DT_LNK) { |
| return FILE_TYPE_SYMLINK; |
| } else { |
| return FILE_TYPE_REGULAR; |
| } |
| } else // NOLINT (the brace is in the next line) |
| #endif |
| { |
| struct stat st; |
| LStatOrDie(path, &st); |
| if (S_ISDIR(st.st_mode)) { |
| return FILE_TYPE_DIRECTORY; |
| } else if (S_ISLNK(st.st_mode)) { |
| return FILE_TYPE_SYMLINK; |
| } else { |
| return FILE_TYPE_REGULAR; |
| } |
| } |
| } |
| |
| void LStatOrDie(const std::string &path, struct stat *st) { |
| if (lstat(path.c_str(), st) != 0) { |
| PDIE("lstating file '%s'", path.c_str()); |
| } |
| } |
| |
| void StatOrDie(const std::string &path, struct stat *st) { |
| if (stat(path.c_str(), st) != 0) { |
| PDIE("stating file '%s'", path.c_str()); |
| } |
| } |
| |
| void ReadLinkOrDie(const std::string &path, std::string *output) { |
| char readlink_buffer[PATH_MAX]; |
| int sz = readlink(path.c_str(), readlink_buffer, sizeof(readlink_buffer)); |
| if (sz < 0) { |
| PDIE("reading symlink '%s'", path.c_str()); |
| } |
| // readlink returns a non-null terminated string. |
| std::string(readlink_buffer, sz).swap(*output); |
| } |
| |
| void EnsureDirReadAndWritePerms(const std::string &path) { |
| const int kMode = 0700; |
| struct stat st; |
| LStatOrDie(path, &st); |
| if ((st.st_mode & kMode) != kMode) { |
| int new_mode = st.st_mode | kMode; |
| if (chmod(path.c_str(), new_mode) != 0) { |
| PDIE("chmod '%s'", path.c_str()); |
| } |
| } |
| } |
| |
| bool DelTree(const std::string &path, FileType file_type) { |
| if (file_type != FILE_TYPE_DIRECTORY) { |
| if (unlink(path.c_str()) != 0) { |
| #if !defined(__CYGWIN__) |
| PDIE("unlinking '%s'", path.c_str()); |
| #endif |
| return false; |
| } |
| return true; |
| } |
| |
| EnsureDirReadAndWritePerms(path); |
| |
| struct dirent *entry; |
| DIR *dh = opendir(path.c_str()); |
| if (!dh) { |
| PDIE("opendir '%s'", path.c_str()); |
| } |
| errno = 0; |
| while ((entry = readdir(dh)) != nullptr) { |
| if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue; |
| const std::string entry_path = path + '/' + entry->d_name; |
| FileType entry_file_type = DentryToFileType(entry_path, entry); |
| DelTree(entry_path, entry_file_type); |
| errno = 0; |
| } |
| if (errno != 0) { |
| PDIE("readdir '%s'", path.c_str()); |
| } |
| closedir(dh); |
| if (rmdir(path.c_str()) != 0) { |
| PDIE("rmdir '%s'", path.c_str()); |
| } |
| return true; |
| } |
| |
| private: |
| std::string output_base_; |
| std::string output_filename_; |
| std::string temp_filename_; |
| |
| FileInfoMap manifest_; |
| }; |
| |
| int main(int argc, char **argv) { |
| argv0 = argv[0]; |
| |
| argc--; argv++; |
| bool allow_relative = false; |
| bool use_metadata = false; |
| |
| while (argc >= 1) { |
| if (strcmp(argv[0], "--allow_relative") == 0) { |
| allow_relative = true; |
| argc--; argv++; |
| } else if (strcmp(argv[0], "--use_metadata") == 0) { |
| use_metadata = true; |
| argc--; argv++; |
| } else { |
| break; |
| } |
| } |
| |
| if (argc != 2) { |
| fprintf(stderr, "usage: %s " |
| "[--allow_relative] [--use_metadata] " |
| "INPUT RUNFILES\n", |
| argv0); |
| return 1; |
| } |
| |
| input_filename = argv[0]; |
| output_base_dir = argv[1]; |
| |
| std::string manifest_file = input_filename; |
| if (input_filename[0] != '/') { |
| char cwd_buf[PATH_MAX]; |
| if (getcwd(cwd_buf, sizeof(cwd_buf)) == nullptr) { |
| PDIE("getcwd failed"); |
| } |
| manifest_file = std::string(cwd_buf) + '/' + manifest_file; |
| } |
| |
| RunfilesCreator runfiles_creator(output_base_dir); |
| runfiles_creator.ReadManifest(manifest_file, allow_relative, use_metadata); |
| runfiles_creator.CreateRunfiles(); |
| |
| return 0; |
| } |