blob: dd559b72fd05a7634d27129e240d7f7189fbcfee [file] [log] [blame]
// 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;
}