diff options
42 files changed, 1001 insertions, 158 deletions
@@ -11,8 +11,10 @@ repo/maloader/ testcase_parse_benchmark_test.go bench-old.out bench-new.out +find_test ninja_test string_piece_test +strutil_bench strutil_test go_src_stamp version.cc diff --git a/.travis.yml b/.travis.yml index f045dd3..2fd6fa8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,10 @@ before_script: - sudo apt-get install -y libstdc++-4.8-dev clang-3.5 ninja-build realpath script: - - make -j4 ckati + - make -j4 ckati ckati_tests - ruby runtest.rb -c - ruby runtest.rb -c -n - ruby runtest.rb -c -n -a + - ./ninja_test + - ./string_piece_test + - ./strutil_test @@ -8,5 +8,6 @@ # # Please keep the list sorted. -Kouhei Sutou <kou@cozmixng.org> Google Inc. +Kouhei Sutou <kou@cozmixng.org> +Po Hu <hupo1985@gmail.com> @@ -79,3 +79,16 @@ cc_test_host { }, }, } + +cc_benchmark_host { + name: "ckati_fileutil_bench", + srcs: [ + "fileutil_bench.cc", + ], + whole_static_libs: ["libckati"], + target: { + linux: { + host_ldlibs: ["-lrt", "-lpthread"], + }, + }, +} diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 1d81fb2..c153787 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -26,6 +26,7 @@ Colin Cross <ccross@google.com> Dan Willemsen <dwillemsen@google.com> Fumitoshi Ukai <ukai@google.com> Kouhei Sutou <kou@cozmixng.org> +Po Hu <hupo1985@gmail.com> Ryo Hashimoto <hashimoto@google.com> Shinichiro Hamaji <hamaji@google.com> Stefan Becker <stefanb@gpartner-nvidia.com> diff --git a/Makefile.ckati b/Makefile.ckati index df4c6a5..e4067bb 100644 --- a/Makefile.ckati +++ b/Makefile.ckati @@ -57,7 +57,8 @@ KATI_CXX_GENERATED_SRCS := \ KATI_CXX_SRCS := $(addprefix $(KATI_SRC_PATH)/,$(KATI_CXX_SRCS)) KATI_CXX_TEST_SRCS := \ $(wildcard $(KATI_SRC_PATH)/*_test.cc) \ - $(wildcard $(KATI_SRC_PATH)/*_bench.cc) + $(filter-out $(KATI_SRC_PATH)/fileutil_bench.cc,\ + $(wildcard $(KATI_SRC_PATH)/*_bench.cc)) KATI_CXX_OBJS := $(patsubst $(KATI_SRC_PATH)/%.cc,$(KATI_INTERMEDIATES_PATH)/%.o,\ $(KATI_CXX_SRCS)) @@ -36,7 +36,8 @@ Evaluator::Evaluator() avoid_io_(false), eval_depth_(0), posix_sym_(Intern(".POSIX")), - is_posix_(false) { + is_posix_(false), + kati_readonly_(Intern(".KATI_READONLY")) { } Evaluator::~Evaluator() { @@ -69,6 +70,8 @@ Var* Evaluator::EvalRHS(Symbol lhs, Value* rhs_v, StringPiece orig_rhs, Var* prev = LookupVarInCurrentScope(lhs); if (!prev->IsDefined()) { rhs = new RecursiveVar(rhs_v, origin, orig_rhs); + } else if (prev->ReadOnly()) { + Error(StringPrintf("*** cannot assign to readonly variable: %s", lhs.c_str())); } else { prev->AppendVar(this, rhs_v); rhs = prev; @@ -101,11 +104,31 @@ void Evaluator::EvalAssign(const AssignStmt* stmt) { Symbol lhs = stmt->GetLhsSymbol(this); if (lhs.empty()) Error("*** empty variable name."); + + if (lhs == kati_readonly_) { + string rhs; + stmt->rhs->Eval(this, &rhs); + for (auto const& name : WordScanner(rhs)) { + Var* var = Intern(name).GetGlobalVar(); + if (!var->IsDefined()) { + Error(StringPrintf("*** unknown variable: %s", name.as_string().c_str())); + } + var->SetReadOnly(); + } + return; + } + Var* rhs = EvalRHS(lhs, stmt->rhs, stmt->orig_rhs, stmt->op, stmt->directive == AssignDirective::OVERRIDE); - if (rhs) + if (rhs) { + bool readonly; lhs.SetGlobalVar(rhs, - stmt->directive == AssignDirective::OVERRIDE); + stmt->directive == AssignDirective::OVERRIDE, + &readonly); + if (readonly) { + Error(StringPrintf("*** cannot assign to readonly variable: %s", lhs.c_str())); + } + } } void Evaluator::EvalRule(const RuleStmt* stmt) { @@ -122,7 +145,10 @@ void Evaluator::EvalRule(const RuleStmt* stmt) { Rule* rule; RuleVarAssignment rule_var; - ParseRule(loc_, expr, stmt->term, &rule, &rule_var); + function<string()> after_term_fn = [this, stmt](){ + return stmt->after_term ? stmt->after_term->Eval(this) : ""; + }; + ParseRule(loc_, expr, stmt->term, after_term_fn, &rule, &rule_var); if (rule) { if (stmt->term == ';') { @@ -164,9 +190,29 @@ void Evaluator::EvalRule(const RuleStmt* stmt) { } current_scope_ = p.first->second; + + if (lhs == kati_readonly_) { + string rhs_value; + rhs->Eval(this, &rhs_value); + for (auto const& name : WordScanner(rhs_value)) { + Var* var = current_scope_->Lookup(Intern(name)); + if (!var->IsDefined()) { + Error(StringPrintf("*** unknown variable: %s", name.as_string().c_str())); + } + var->SetReadOnly(); + } + current_scope_ = NULL; + continue; + } + Var* rhs_var = EvalRHS(lhs, rhs, StringPiece("*TODO*"), rule_var.op); - if (rhs_var) - current_scope_->Assign(lhs, new RuleVar(rhs_var, rule_var.op)); + if (rhs_var) { + bool readonly; + current_scope_->Assign(lhs, new RuleVar(rhs_var, rule_var.op), &readonly); + if (readonly) { + Error(StringPrintf("*** cannot assign to readonly variable: %s", lhs.c_str())); + } + } current_scope_ = NULL; } } @@ -125,6 +125,8 @@ class Evaluator { bool is_posix_; static unordered_set<Symbol> used_undefined_vars_; + + Symbol kati_readonly_; }; #endif // EVAL_H_ @@ -46,7 +46,8 @@ class Executor { explicit Executor(Evaluator* ev) : ce_(ev), num_commands_(0) { - shell_ = ev->GetShellAndFlag(); + shell_ = ev->GetShell(); + shellflag_ = ev->GetShellFlag(); } double ExecNode(DepNode* n, DepNode* needed_by) { @@ -106,7 +107,8 @@ class Executor { } if (!g_flags.is_dry_run) { string out; - int result = RunCommand(shell_, command->cmd.c_str(), + int result = RunCommand(shell_, shellflag_, + command->cmd.c_str(), RedirectStderr::STDOUT, &out); printf("%s", out.c_str()); @@ -136,6 +138,7 @@ class Executor { CommandEvaluator ce_; unordered_map<Symbol, double> done_; string shell_; + string shellflag_; uint64_t num_commands_; }; diff --git a/fileutil.cc b/fileutil.cc index 0d3c2d6..4cf653b 100644 --- a/fileutil.cc +++ b/fileutil.cc @@ -60,15 +60,24 @@ double GetTimestamp(StringPiece filename) { return GetTimestampFromStat(st); } -int RunCommand(const string& shell, const string& cmd, - RedirectStderr redirect_stderr, +int RunCommand(const string& shell, const string& shellflag, + const string& cmd, RedirectStderr redirect_stderr, string* s) { - string cmd_escaped = cmd; - EscapeShell(&cmd_escaped); - string cmd_with_shell = shell + " \"" + cmd_escaped + "\""; - const char* argv[] = { - "/bin/sh", "-c", cmd_with_shell.c_str(), NULL - }; + const char* argv[] = { NULL, NULL, NULL, NULL }; + string cmd_with_shell; + if (shell[0] != '/' || shell.find_first_of(" $") != string::npos) { + string cmd_escaped = cmd; + EscapeShell(&cmd_escaped); + cmd_with_shell = shell + " " + shellflag + " \"" + cmd_escaped + "\""; + argv[0] = "/bin/sh"; + argv[1] = "-c"; + argv[2] = cmd_with_shell.c_str(); + } else { + // If the shell isn't complicated, we don't need to wrap in /bin/sh + argv[0] = shell.c_str(); + argv[1] = shellflag.c_str(); + argv[2] = cmd.c_str(); + } int pipefd[2]; if (pipe(pipefd) != 0) @@ -36,8 +36,8 @@ enum struct RedirectStderr { DEV_NULL, }; -int RunCommand(const string& shell, const string& cmd, - RedirectStderr redirect_stderr, +int RunCommand(const string& shell, const string& shellflag, + const string& cmd, RedirectStderr redirect_stderr, string* out); void GetExecutablePath(string* path); diff --git a/fileutil_bench.cc b/fileutil_bench.cc new file mode 100644 index 0000000..4b1f033 --- /dev/null +++ b/fileutil_bench.cc @@ -0,0 +1,41 @@ +// Copyright 2016 Google Inc. 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. + +#include "fileutil.h" +#include <benchmark/benchmark.h> +#include <cstdio> + +static void BM_RunCommand(benchmark::State& state) { + std::string shell = "/bin/bash"; + std::string shellflag = "-c"; + std::string cmd = "echo $((1+3))"; + while (state.KeepRunning()) { + std::string result; + RunCommand(shell, shellflag, cmd, RedirectStderr::NONE, &result); + } +} +BENCHMARK(BM_RunCommand); + +static void BM_RunCommand_ComplexShell(benchmark::State& state) { + std::string shell = "/bin/bash "; + std::string shellflag = "-c"; + std::string cmd = "echo $((1+3))"; + while (state.KeepRunning()) { + std::string result; + RunCommand(shell, shellflag, cmd, RedirectStderr::NONE, &result); + } +} +BENCHMARK(BM_RunCommand_ComplexShell); + +BENCHMARK_MAIN(); @@ -585,7 +585,7 @@ class FindCommandParser { } else if (tok.find_first_of("|;&><*'\"") != string::npos) { return false; } else { - fc_->finddirs.push_back(tok); + fc_->finddirs.push_back(tok.as_string()); } } } @@ -594,7 +594,7 @@ class FindCommandParser { fc_->type = FindCommandType::FINDLEAVES; fc_->follows_symlinks = true; StringPiece tok; - vector<StringPiece> findfiles; + vector<string> findfiles; while (true) { if (!GetNextToken(&tok)) return false; @@ -604,13 +604,13 @@ class FindCommandParser { if (findfiles.size() < 2) return false; fc_->finddirs.swap(findfiles); - fc_->print_cond.reset(new NameCond(fc_->finddirs.back().as_string())); + fc_->print_cond.reset(new NameCond(fc_->finddirs.back())); fc_->finddirs.pop_back(); } else { if (findfiles.size() < 1) return false; for (auto& file : findfiles) { - FindCond* cond = new NameCond(file.as_string()); + FindCond* cond = new NameCond(file); if (fc_->print_cond.get()) { cond = new OrCond(fc_->print_cond.release(), cond); } @@ -640,12 +640,12 @@ class FindCommandParser { fc_->mindepth = d; } else if (HasPrefix(tok, "--dir=")) { StringPiece dir= tok.substr(strlen("--dir=")); - fc_->finddirs.push_back(dir); + fc_->finddirs.push_back(dir.as_string()); } else if (HasPrefix(tok, "--")) { WARN("Unknown flag in findleaves.py: %.*s", SPF(tok)); return false; } else { - findfiles.push_back(tok); + findfiles.push_back(tok.as_string()); } } } @@ -788,7 +788,7 @@ class FindEmulatorImpl : public FindEmulator { } const size_t orig_out_size = out->size(); - for (StringPiece finddir : fc.finddirs) { + for (const string& finddir : fc.finddirs) { const string dir = ConcatDir(fc.chdir, finddir); if (!CanHandle(dir)) { @@ -813,7 +813,7 @@ class FindEmulatorImpl : public FindEmulator { continue; } - string path = finddir.as_string(); + string path = finddir; unordered_map<const DirentNode*, string> cur_read_dirs; if (!base->RunFind(fc, 0, &path, &cur_read_dirs, out)) { LOG("FindEmulator: RunFind failed: %s", cmd.c_str()); @@ -41,7 +41,7 @@ struct FindCommand { FindCommandType type; string chdir; string testdir; - vector<StringPiece> finddirs; + vector<string> finddirs; bool follows_symlinks; unique_ptr<FindCond> print_cond; unique_ptr<FindCond> prune_cond; @@ -16,6 +16,7 @@ #include "flags.h" +#include <stdlib.h> #include <unistd.h> #include "log.h" @@ -52,6 +53,13 @@ void Flags::Parse(int argc, char** argv) { num_jobs = num_cpus = sysconf(_SC_NPROCESSORS_ONLN); const char* num_jobs_str; + if (const char* makeflags = getenv("MAKEFLAGS")) { + for (StringPiece tok : WordScanner(makeflags)) { + if (!HasPrefix(tok, "-") && tok.find('=') != string::npos) + cl_vars.push_back(tok); + } + } + for (int i = 1; i < argc; i++) { const char* arg = argv[i]; bool should_propagate = true; @@ -78,10 +86,13 @@ void Flags::Parse(int argc, char** argv) { } else if (!strcmp(arg, "--regen")) { // TODO: Make this default. regen = true; + } else if (!strcmp(arg, "--regen_debug")) { + regen_debug = true; } else if (!strcmp(arg, "--regen_ignoring_kati_binary")) { regen_ignoring_kati_binary = true; } else if (!strcmp(arg, "--dump_kati_stamp")) { dump_kati_stamp = true; + regen_debug = true; } else if (!strcmp(arg, "--detect_android_echo")) { detect_android_echo = true; } else if (!strcmp(arg, "--detect_depfiles")) { @@ -36,6 +36,7 @@ struct Flags { bool is_silent_mode; bool is_syntax_check_only; bool regen; + bool regen_debug; bool regen_ignoring_kati_binary; bool use_find_emulator; const char* goma_dir; @@ -17,9 +17,11 @@ #include "func.h" #include <errno.h> +#include <fcntl.h> #include <limits.h> #include <stdio.h> #include <stdlib.h> +#include <sys/stat.h> #include <unistd.h> #include <algorithm> @@ -491,8 +493,8 @@ static bool HasNoIoInShellScript(const string& cmd) { return false; } -static void ShellFuncImpl(const string& shell, const string& cmd, - string* s, FindCommand** fc) { +static void ShellFuncImpl(const string& shell, const string& shellflag, + const string& cmd, string* s, FindCommand** fc) { LOG("ShellFunc: %s", cmd.c_str()); #ifdef TEST_FIND_EMULATOR @@ -517,7 +519,7 @@ static void ShellFuncImpl(const string& shell, const string& cmd, } COLLECT_STATS_WITH_SLOW_REPORT("func shell time", cmd.c_str()); - RunCommand(shell, cmd, RedirectStderr::NONE, s); + RunCommand(shell, shellflag, cmd, RedirectStderr::NONE, s); FormatForCommandSubstitution(s); #ifdef TEST_FIND_EMULATOR @@ -533,7 +535,9 @@ static void ShellFuncImpl(const string& shell, const string& cmd, static vector<CommandResult*> g_command_results; bool ShouldStoreCommandResult(StringPiece cmd) { - if (HasWord(cmd, "date") || HasWord(cmd, "echo")) + // We really just want to ignore this one, or remove BUILD_DATETIME from + // Android completely + if (cmd == "date +%s") return false; Pattern pat(g_flags.ignore_dirty_pattern); @@ -562,14 +566,17 @@ void ShellFunc(const vector<Value*>& args, Evaluator* ev, string* s) { return; } - const string&& shell = ev->GetShellAndFlag(); + const string&& shell = ev->GetShell(); + const string&& shellflag = ev->GetShellFlag(); string out; FindCommand* fc = NULL; - ShellFuncImpl(shell, cmd, &out, &fc); + ShellFuncImpl(shell, shellflag, cmd, &out, &fc); if (ShouldStoreCommandResult(cmd)) { CommandResult* cr = new CommandResult(); + cr->op = (fc == NULL) ? CommandOp::SHELL : CommandOp::FIND, cr->shell = shell; + cr->shellflag = shellflag; cr->cmd = cmd; cr->find.reset(fc); cr->result = out; @@ -684,6 +691,124 @@ void ErrorFunc(const vector<Value*>& args, Evaluator* ev, string*) { ev->Error(StringPrintf("*** %s.", a.c_str())); } +static void FileReadFunc(Evaluator* ev, const string& filename, string* s) { + int fd = open(filename.c_str(), O_RDONLY); + if (fd < 0) { + if (errno == ENOENT) { + if (ShouldStoreCommandResult(filename)) { + CommandResult* cr = new CommandResult(); + cr->op = CommandOp::READ_MISSING; + cr->cmd = filename; + g_command_results.push_back(cr); + } + return; + } else { + ev->Error("*** open failed."); + } + } + + struct stat st; + if (fstat(fd, &st) < 0) { + ev->Error("*** fstat failed."); + } + + size_t len = st.st_size; + string out; + out.resize(len); + ssize_t r = HANDLE_EINTR(read(fd, &out[0], len)); + if (r != static_cast<ssize_t>(len)) { + ev->Error("*** read failed."); + } + + if (close(fd) < 0) { + ev->Error("*** close failed."); + } + + if (out.back() == '\n') { + out.pop_back(); + } + + if (ShouldStoreCommandResult(filename)) { + CommandResult* cr = new CommandResult(); + cr->op = CommandOp::READ; + cr->cmd = filename; + g_command_results.push_back(cr); + } + *s += out; +} + +static void FileWriteFunc(Evaluator* ev, const string& filename, bool append, string text) { + FILE* f = fopen(filename.c_str(), append ? "ab" : "wb"); + if (f == NULL) { + ev->Error("*** fopen failed."); + } + + if (fwrite(&text[0], text.size(), 1, f) != 1) { + ev->Error("*** fwrite failed."); + } + + if (fclose(f) != 0) { + ev->Error("*** fclose failed."); + } + + if (ShouldStoreCommandResult(filename)) { + CommandResult* cr = new CommandResult(); + cr->op = CommandOp::WRITE; + cr->cmd = filename; + cr->result = text; + g_command_results.push_back(cr); + } +} + +void FileFunc(const vector<Value*>& args, Evaluator* ev, string* s) { + if (ev->avoid_io()) { + ev->Error("*** $(file ...) is not supported in rules."); + } + + string arg = args[0]->Eval(ev); + StringPiece filename = TrimSpace(arg); + + if (filename.size() <= 1) { + ev->Error("*** Missing filename"); + } + + if (filename[0] == '<') { + filename = TrimLeftSpace(filename.substr(1)); + if (!filename.size()) { + ev->Error("*** Missing filename"); + } + if (args.size() > 1) { + ev->Error("*** invalid argument"); + } + + FileReadFunc(ev, filename.as_string(), s); + } else if (filename[0] == '>') { + bool append = false; + if (filename[1] == '>') { + append = true; + filename = filename.substr(2); + } else { + filename = filename.substr(1); + } + filename = TrimLeftSpace(filename); + if (!filename.size()) { + ev->Error("*** Missing filename"); + } + + string text; + if (args.size() > 1) { + text = args[1]->Eval(ev); + if (text.size() == 0 || text.back() != '\n') { + text.push_back('\n'); + } + } + + FileWriteFunc(ev, filename.as_string(), append, text); + } else { + ev->Error(StringPrintf("*** Invalid file operation: %s. Stop.", filename.as_string().c_str())); + } +} + FuncInfo g_func_infos[] = { { "patsubst", &PatsubstFunc, 3, 3, false, false }, { "strip", &StripFunc, 1, 1, false, false }, @@ -725,6 +850,8 @@ FuncInfo g_func_infos[] = { { "info", &InfoFunc, 1, 1, false, false }, { "warning", &WarningFunc, 1, 1, false, false }, { "error", &ErrorFunc, 1, 1, false, false }, + + { "file", &FileFunc, 2, 1, false, false }, }; unordered_map<StringPiece, FuncInfo*>* g_func_info_map; @@ -41,8 +41,19 @@ FuncInfo* GetFuncInfo(StringPiece name); struct FindCommand; +enum struct CommandOp { + SHELL, + FIND, + READ, + READ_MISSING, + WRITE, + APPEND, +}; + struct CommandResult { + CommandOp op; string shell; + string shellflag; string cmd; unique_ptr<FindCommand> find; string result; @@ -483,6 +483,9 @@ class NinjaGenerator { string rule_name = "phony"; bool use_local_pool = false; + if (node->output.get(0) == '.') { + return; + } if (g_flags.enable_debug) { *o << "# " << (node->loc.filename ? node->loc.filename : "(null)") << ':' << node->loc.lineno << "\n"; @@ -733,31 +736,28 @@ class NinjaGenerator { const vector<CommandResult*>& crs = GetShellCommandResults(); DumpInt(fp, crs.size()); for (CommandResult* cr : crs) { + DumpInt(fp, static_cast<int>(cr->op)); DumpString(fp, cr->shell); + DumpString(fp, cr->shellflag); DumpString(fp, cr->cmd); DumpString(fp, cr->result); - if (!cr->find.get()) { - // Always re-run this command. - DumpInt(fp, 0); - continue; - } - - DumpInt(fp, 1); - vector<string> missing_dirs; - for (StringPiece fd : cr->find->finddirs) { - const string& d = ConcatDir(cr->find->chdir, fd); - if (!Exists(d)) - missing_dirs.push_back(d); - } - DumpInt(fp, missing_dirs.size()); - for (const string& d : missing_dirs) { - DumpString(fp, d); - } + if (cr->op == CommandOp::FIND) { + vector<string> missing_dirs; + for (StringPiece fd : cr->find->finddirs) { + const string& d = ConcatDir(cr->find->chdir, fd); + if (!Exists(d)) + missing_dirs.push_back(d); + } + DumpInt(fp, missing_dirs.size()); + for (const string& d : missing_dirs) { + DumpString(fp, d); + } - DumpInt(fp, cr->find->read_dirs->size()); - for (StringPiece s : *cr->find->read_dirs) { - DumpString(fp, ConcatDir(cr->find->chdir, s)); + DumpInt(fp, cr->find->read_dirs->size()); + for (StringPiece s : *cr->find->read_dirs) { + DumpString(fp, ConcatDir(cr->find->chdir, s)); + } } } @@ -23,6 +23,7 @@ #include "fileutil.h" #include "find.h" +#include "func.h" #include "io.h" #include "log.h" #include "ninja.h" @@ -52,12 +53,13 @@ class StampChecker { }; struct ShellResult { + CommandOp op; string shell; + string shellflag; string cmd; string result; vector<string> missing_dirs; vector<string> read_dirs; - bool has_condition; }; public: @@ -132,7 +134,7 @@ class StampChecker { const string& stamp_filename = GetNinjaStampFilename(); FILE* fp = fopen(stamp_filename.c_str(), "rb"); if (!fp) { - if (g_flags.dump_kati_stamp) + if (g_flags.regen_debug) printf("%s: %s\n", stamp_filename.c_str(), strerror(errno)); return true; } @@ -145,7 +147,7 @@ class StampChecker { fprintf(stderr, "incomplete kati_stamp, regenerating...\n"); RETURN_TRUE; } - if (g_flags.dump_kati_stamp) + if (g_flags.regen_debug) printf("Generated time: %f\n", gen_time); string s, s2; @@ -163,7 +165,7 @@ class StampChecker { } } if (ShouldIgnoreDirty(s)) { - if (g_flags.dump_kati_stamp) + if (g_flags.regen_debug) printf("file %s: ignored (%f)\n", s.c_str(), ts); continue; } @@ -231,22 +233,23 @@ class StampChecker { for (int i = 0; i < num_crs; i++) { ShellResult* sr = new ShellResult; commands_.push_back(sr); + sr->op = static_cast<CommandOp>(LOAD_INT(fp)); LOAD_STRING(fp, &sr->shell); + LOAD_STRING(fp, &sr->shellflag); LOAD_STRING(fp, &sr->cmd); LOAD_STRING(fp, &sr->result); - sr->has_condition = LOAD_INT(fp); - if (!sr->has_condition) - continue; - int num_missing_dirs = LOAD_INT(fp); - for (int j = 0; j < num_missing_dirs; j++) { - LOAD_STRING(fp, &s); - sr->missing_dirs.push_back(s); - } - int num_read_dirs = LOAD_INT(fp); - for (int j = 0; j < num_read_dirs; j++) { - LOAD_STRING(fp, &s); - sr->read_dirs.push_back(s); + if (sr->op == CommandOp::FIND) { + int num_missing_dirs = LOAD_INT(fp); + for (int j = 0; j < num_missing_dirs; j++) { + LOAD_STRING(fp, &s); + sr->missing_dirs.push_back(s); + } + int num_read_dirs = LOAD_INT(fp); + for (int j = 0; j < num_read_dirs; j++) { + LOAD_STRING(fp, &s); + sr->read_dirs.push_back(s); + } } } @@ -292,7 +295,7 @@ class StampChecker { } bool ShouldRunCommand(const ShellResult* sr) { - if (!sr->has_condition) + if (sr->op != CommandOp::FIND) return true; COLLECT_STATS("stat time (regen)"); @@ -324,8 +327,56 @@ class StampChecker { } bool CheckShellResult(const ShellResult* sr, string* err) { - if (!ShouldRunCommand(sr)) { + if (sr->op == CommandOp::READ_MISSING) { + if (Exists(sr->cmd)) { + if (g_flags.dump_kati_stamp) + printf("file %s: dirty\n", sr->cmd.c_str()); + else + *err = StringPrintf("$(file <%s) was changed, regenerating...\n", + sr->cmd.c_str()); + return true; + } if (g_flags.dump_kati_stamp) + printf("file %s: clean\n", sr->cmd.c_str()); + return false; + } + + if (sr->op == CommandOp::READ) { + double ts = GetTimestamp(sr->cmd); + if (gen_time_ < ts) { + if (g_flags.dump_kati_stamp) + printf("file %s: dirty\n", sr->cmd.c_str()); + else + *err = StringPrintf("$(file <%s) was changed, regenerating...\n", + sr->cmd.c_str()); + return true; + } + if (g_flags.dump_kati_stamp) + printf("file %s: clean\n", sr->cmd.c_str()); + return false; + } + + if (sr->op == CommandOp::WRITE || sr->op == CommandOp::APPEND) { + FILE* f = fopen(sr->cmd.c_str(), (sr->op == CommandOp::WRITE) ? "wb" : "ab"); + if (f == NULL) { + PERROR("fopen"); + } + + if (fwrite(&sr->result[0], sr->result.size(), 1, f) != 1) { + PERROR("fwrite"); + } + + if (fclose(f) != 0) { + PERROR("fclose"); + } + + if (g_flags.dump_kati_stamp) + printf("file %s: clean (write)\n", sr->cmd.c_str()); + return false; + } + + if (!ShouldRunCommand(sr)) { + if (g_flags.regen_debug) printf("shell %s: clean (no rerun)\n", sr->cmd.c_str()); return false; } @@ -340,7 +391,7 @@ class StampChecker { COLLECT_STATS_WITH_SLOW_REPORT("shell time (regen)", sr->cmd.c_str()); string result; - RunCommand(sr->shell, sr->cmd, RedirectStderr::DEV_NULL, &result); + RunCommand(sr->shell, sr->shellflag, sr->cmd, RedirectStderr::DEV_NULL, &result); FormatForCommandSubstitution(&result); if (sr->result != result) { if (g_flags.dump_kati_stamp) { @@ -351,7 +402,7 @@ class StampChecker { //*err += StringPrintf("%s => %s\n", expected.c_str(), result.c_str()); } return true; - } else if (g_flags.dump_kati_stamp) { + } else if (g_flags.regen_debug) { printf("shell %s: clean (rerun)\n", sr->cmd.c_str()); } return false; @@ -375,8 +426,8 @@ class StampChecker { } }); - for (ShellResult* sr : commands_) { - tp->Submit([this, sr]() { + tp->Submit([this]() { + for (ShellResult* sr : commands_) { string err; if (CheckShellResult(sr, &err)) { unique_lock<mutex> lock(mu_); @@ -385,8 +436,8 @@ class StampChecker { msg_ = err; } } - }); - } + } + }); tp->Wait(); if (needs_regen_) { @@ -54,6 +54,7 @@ Rule::Rule() } void ParseRule(Loc& loc, StringPiece line, char term, + function<string()> after_term_fn, Rule** out_rule, RuleVarAssignment* rule_var) { size_t index = line.find(':'); if (index == string::npos) { @@ -84,15 +85,31 @@ void ParseRule(Loc& loc, StringPiece line, char term, StringPiece rest = line.substr(index); size_t term_index = rest.find_first_of("=;"); + string buf; if ((term_index != string::npos && rest[term_index] == '=') || (term_index == string::npos && term == '=')) { if (term_index == string::npos) term_index = rest.size(); - rule_var->outputs.swap(outputs); - ParseAssignStatement(rest, term_index, - &rule_var->lhs, &rule_var->rhs, &rule_var->op); - *out_rule = NULL; - return; + // "test: =foo" is questionable but a valid rule definition (not a + // target specific variable). + // See https://github.com/google/kati/issues/83 + if (term_index == 0) { + KATI_WARN("%s:%d: defining a target which starts with `=', " + "which is not probably what you meant", LOCF(loc)); + buf = line.as_string(); + if (term) + buf += term; + buf += after_term_fn(); + line = buf; + rest = line.substr(index); + term_index = string::npos; + } else { + rule_var->outputs.swap(outputs); + ParseAssignStatement(rest, term_index, + &rule_var->lhs, &rule_var->rhs, &rule_var->op); + *out_rule = NULL; + return; + } } Rule* rule = new Rule(); @@ -15,6 +15,8 @@ #ifndef RULE_H_ #define RULE_H_ +#include <functional> +#include <string> #include <vector> #include "loc.h" @@ -58,8 +60,12 @@ struct RuleVarAssignment { AssignOp op; }; -// If |rule| is not NULL, |rule_var| is filled. +// If |rule| is not NULL, |rule_var| is filled. If the expression +// after the terminator |term| is needed (this happens only when +// |term| is '='), |after_term_fn| will be called to obtain the right +// hand side. void ParseRule(Loc& loc, StringPiece line, char term, + function<string()> after_term_fn, Rule** rule, RuleVarAssignment* rule_var); #endif // RULE_H_ @@ -21,6 +21,7 @@ #include <unistd.h> #include <algorithm> +#include <functional> #include <stack> #include <utility> @@ -38,8 +39,9 @@ static bool isSpace(char c) { static int SkipUntilSSE42(const char* s, int len, const char* ranges, int ranges_size) { __m128i ranges16 = _mm_loadu_si128((const __m128i*)ranges); + len &= ~15; int i = 0; - do { + while (i < len) { __m128i b16 = _mm_loadu_si128((const __m128i*)(s + i)); int r = _mm_cmpestri( ranges16, ranges_size, b16, len - i, @@ -48,11 +50,26 @@ static int SkipUntilSSE42(const char* s, int len, return i + r; } i += 16; - } while (i < len); + } return len; } #endif +template <typename Cond> +static int SkipUntil(const char* s, int len, + const char* ranges UNUSED, int ranges_size UNUSED, + Cond cond) { + int i = 0; +#ifdef __SSE4_2__ + i += SkipUntilSSE42(s, len, ranges, ranges_size); +#endif + for (; i < len; i++) { + if (cond(s[i])) + break; + } + return i; +} + WordScanner::Iterator& WordScanner::Iterator::operator++() { int len = static_cast<int>(in->size()); for (s = i + 1; s < len; s++) { @@ -66,17 +83,11 @@ WordScanner::Iterator& WordScanner::Iterator::operator++() { return *this; } -#ifdef __SSE4_2__ static const char ranges[] = "\x09\x0d "; - i = s; - i += SkipUntilSSE42(in->data() + s, len - s, ranges, 4); -#else - for (i = s; i < len; i++) { - if (isSpace((*in)[i])) - break; - } -#endif - + // It's intentional we are not using isSpace here. It seems with + // lambda the compiler generates better code. + i = s + SkipUntil(in->data() + s, len - s, ranges, 4, + [](char c) { return (9 <= c && c <= 13) || c == 32; }); return *this; } @@ -423,10 +434,10 @@ size_t FindThreeOutsideParen(StringPiece s, char c1, char c2, char c3) { } size_t FindEndOfLine(StringPiece s, size_t e, size_t* lf_cnt) { -#ifdef __SSE4_2__ static const char ranges[] = "\0\0\n\n\\\\"; while (e < s.size()) { - e += SkipUntilSSE42(s.data() + e, s.size() - e, ranges, 6); + e += SkipUntil(s.data() + e, s.size() - e, ranges, 6, + [](char c) { return c == 0 || c == '\n' || c == '\\'; }); if (e >= s.size()) { CHECK(s.size() == e); break; @@ -452,24 +463,6 @@ size_t FindEndOfLine(StringPiece s, size_t e, size_t* lf_cnt) { } } return e; -#else - bool prev_backslash = false; - for (; e < s.size(); e++) { - char c = s[e]; - if (c == '\\') { - prev_backslash = !prev_backslash; - } else if (c == '\n') { - ++*lf_cnt; - if (!prev_backslash) { - return e; - } - prev_backslash = false; - } else if (c != '\r') { - prev_backslash = false; - } - } - return e; -#endif } StringPiece TrimLeadingCurdir(StringPiece s) { @@ -528,11 +521,14 @@ string EchoEscape(const string str) { return buf; } +static bool NeedsShellEscape(char c) { + return c == 0 || c == '"' || c == '$' || c == '\\' || c == '`'; +} + void EscapeShell(string* s) { -#ifdef __SSE4_2__ static const char ranges[] = "\0\0\"\"$$\\\\``"; size_t prev = 0; - size_t i = SkipUntilSSE42(s->c_str(), s->size(), ranges, 10); + size_t i = SkipUntil(s->c_str(), s->size(), ranges, 10, NeedsShellEscape); if (i == s->size()) return; @@ -550,37 +546,8 @@ void EscapeShell(string* s) { r += c; i++; prev = i; - i += SkipUntilSSE42(s->c_str() + i, s->size() - i, ranges, 10); + i += SkipUntil(s->c_str() + i, s->size() - i, ranges, 10, NeedsShellEscape); } StringPiece(*s).substr(prev).AppendToString(&r); s->swap(r); -#else - if (s->find_first_of("$`\\\"") == string::npos) - return; - string r; - bool last_dollar = false; - for (char c : *s) { - switch (c) { - case '$': - if (last_dollar) { - r += c; - last_dollar = false; - } else { - r += '\\'; - r += c; - last_dollar = true; - } - break; - case '`': - case '"': - case '\\': - r += '\\'; - // fall through. - default: - r += c; - last_dollar = false; - } - } - s->swap(r); -#endif } diff --git a/strutil_test.cc b/strutil_test.cc index a89786f..7a5a47d 100644 --- a/strutil_test.cc +++ b/strutil_test.cc @@ -17,6 +17,8 @@ #include "strutil.h" #include <assert.h> +#include <sys/mman.h> +#include <unistd.h> #include <string> #include <vector> @@ -138,6 +140,50 @@ void TestFindEndOfLine() { ASSERT_EQ(FindEndOfLine(StringPiece(buf, 2), 0, &lf_cnt), 2); } +// Take a string, and copy it into an allocated buffer where +// the byte immediately after the null termination character +// is read protected. Useful for testing, but doesn't support +// freeing the allocated pages. +const char* CreateProtectedString(const char* str) { + int pagesize = sysconf(_SC_PAGE_SIZE); + void *buffer; + char *buffer_str; + + // Allocate two pages of memory + if (posix_memalign(&buffer, pagesize, pagesize * 2) != 0) { + perror("posix_memalign failed"); + assert(false); + } + + // Make the second page unreadable + buffer_str = (char*)buffer + pagesize; + if (mprotect(buffer_str, pagesize, PROT_NONE) != 0) { + perror("mprotect failed"); + assert(false); + } + + // Then move the test string into the very end of the first page + buffer_str -= strlen(str) + 1; + strcpy(buffer_str, str); + + return buffer_str; +} + +void TestWordScannerInvalidAccess() { + vector<StringPiece> ss; + for (StringPiece tok : WordScanner(CreateProtectedString("0123 456789"))) { + ss.push_back(tok); + } + assert(ss.size() == 2LU); + ASSERT_EQ(ss[0], "0123"); + ASSERT_EQ(ss[1], "456789"); +} + +void TestFindEndOfLineInvalidAccess() { + size_t lf_cnt = 0; + ASSERT_EQ(FindEndOfLine(CreateProtectedString("a\\"), 0, &lf_cnt), 2); +} + } // namespace int main() { @@ -150,5 +196,7 @@ int main() { TestNormalizePath(); TestEscapeShell(); TestFindEndOfLine(); + TestWordScannerInvalidAccess(); + TestFindEndOfLineInvalidAccess(); assert(!g_failed); } @@ -59,16 +59,29 @@ Var* Symbol::GetGlobalVar() const { return v; } -void Symbol::SetGlobalVar(Var* v, bool is_override) const { +void Symbol::SetGlobalVar(Var* v, bool is_override, bool* readonly) const { if (static_cast<size_t>(v_) >= g_symbol_data.size()) { g_symbol_data.resize(v_ + 1); } Var* orig = g_symbol_data[v_].gv; + if (orig->ReadOnly()) { + if (readonly != nullptr) + *readonly = true; + else + ERROR("*** cannot assign to readonly variable: %s", c_str()); + return; + } else if (readonly != nullptr) { + *readonly = false; + } if (!is_override && (orig->Origin() == VarOrigin::OVERRIDE || orig->Origin() == VarOrigin::ENVIRONMENT_OVERRIDE)) { return; } + if (orig->Origin() == VarOrigin::COMMAND_LINE && + v->Origin() == VarOrigin::FILE) { + return; + } if (orig->Origin() == VarOrigin::AUTOMATIC) { ERROR("overriding automatic variable is not implemented yet"); } @@ -56,7 +56,7 @@ class Symbol { bool IsValid() const { return v_ >= 0; } Var* GetGlobalVar() const; - void SetGlobalVar(Var* v, bool is_override = false) const; + void SetGlobalVar(Var* v, bool is_override = false, bool* readonly = nullptr) const; private: explicit Symbol(int v); diff --git a/testcase/cmdline_var.sh b/testcase/cmdline_var.sh new file mode 100755 index 0000000..b892736 --- /dev/null +++ b/testcase/cmdline_var.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# +# Copyright 2016 Google Inc. 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. + +set -e + +mk="$@" + +cat <<EOF > Makefile +CLVAR := FAIL +all: + @echo \$(CLVAR) +EOF + +${mk} CLVAR:=PASS 2> /dev/null diff --git a/testcase/cmdline_var_makeflags.sh b/testcase/cmdline_var_makeflags.sh new file mode 100755 index 0000000..6e38a2d --- /dev/null +++ b/testcase/cmdline_var_makeflags.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# +# Copyright 2016 Google Inc. 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. + +set -e + +mk="$@" + +cat <<EOF > Makefile +CLVAR := FAIL +MFVAR := FAIL +FILEVAR := PASS +all: + @echo \$(ENVVAR) \$(origin ENVVAR) + @echo \$(MFVAR) \$(origin MFVAR) + @echo \$(CLVAR) \$(origin CLVAR) + @echo \$(FILEVAR) \$(origin FILEVAR) +EOF + +export ENVVAR=PASS +export FILEVAR=FAIL +export MAKEFLAGS="MFVAR=PASS CLVAR=FAIL" + +${mk} CLVAR=PASS 2> /dev/null diff --git a/testcase/cmdline_var_modify.sh b/testcase/cmdline_var_modify.sh new file mode 100755 index 0000000..9616a2b --- /dev/null +++ b/testcase/cmdline_var_modify.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# +# Copyright 2016 Google Inc. 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. + +set -e + +mk="$@" + +cat <<EOF > Makefile +CLVAR := FAIL +all: + @echo \$(CLVAR) +EOF + +${mk} CLVAR:=P CLVAR+=A CLVAR+=SS CLVAR?=FAIL 2> /dev/null diff --git a/testcase/cmdline_var_override.sh b/testcase/cmdline_var_override.sh new file mode 100755 index 0000000..0a5b3c1 --- /dev/null +++ b/testcase/cmdline_var_override.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# +# Copyright 2016 Google Inc. 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. + +set -e + +mk="$@" + +cat <<EOF > Makefile +override CLVAR := PASS +all: + @echo \$(CLVAR) +EOF + +${mk} CLVAR:=FAIL 2> /dev/null diff --git a/testcase/empty_target_specific_var.mk b/testcase/empty_target_specific_var.mk new file mode 100644 index 0000000..fd9e6c1 --- /dev/null +++ b/testcase/empty_target_specific_var.mk @@ -0,0 +1,7 @@ +# TODO(go): https://github.com/google/kati/issues/83 + +test: =foo + +var==foo +$(var): + echo PASS diff --git a/testcase/empty_target_specific_var2.mk b/testcase/empty_target_specific_var2.mk new file mode 100644 index 0000000..6defb52 --- /dev/null +++ b/testcase/empty_target_specific_var2.mk @@ -0,0 +1,11 @@ +# TODO(go): https://github.com/google/kati/issues/83 + +define var +VAR:=1 +endef + +$(call var) + +eq_one:==1 +$(eq_one): + echo PASS diff --git a/testcase/file_func.sh b/testcase/file_func.sh new file mode 100755 index 0000000..72935dc --- /dev/null +++ b/testcase/file_func.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# +# Copyright 2016 Google Inc. 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. + +set -e + +mk="$@" + +echo "PASS" >testfile + +cat <<EOF > Makefile +ifdef KATI +SUPPORTS_FILE := 1 +endif +ifneq (,\$(filter 4.2%,\$(MAKE_VERSION))) +SUPPORTS_FILE := 1 +endif + +ifdef SUPPORTS_FILE + \$(file >testwrite,PASS) + \$(info Read not found: \$(if \$(file <notfound),FAIL,PASS)) + \$(info Read: \$(file < testfile)) + \$(info Read back: \$(file <testwrite)) +else + # Make <4 does not support \$(file ...) + \$(info Read not found: PASS) + \$(info Read: PASS) + \$(info Read back: PASS) +endif + +.PHONY: all +all: +EOF + +${mk} 2>&1 diff --git a/testcase/ninja_regen_filefunc_read.sh b/testcase/ninja_regen_filefunc_read.sh new file mode 100755 index 0000000..7fc3a9f --- /dev/null +++ b/testcase/ninja_regen_filefunc_read.sh @@ -0,0 +1,74 @@ +#!/bin/sh +# +# Copyright 2016 Google Inc. 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. + +set -e + +log=/tmp/log +mk="$@" + +sleep_if_necessary() { + if [ x$(uname) != x"Linux" -o x"${TRAVIS}" != x"" ]; then + sleep "$@" + fi +} + +cat <<EOF > Makefile +A := \$(file <file_a) +all: + echo foo +EOF + +${mk} 2> ${log} +if [ -e ninja.sh ]; then + ./ninja.sh +fi + +${mk} 2> ${log} +if [ -e ninja.sh ]; then + if grep regenerating ${log}; then + echo 'Should not be regenerated' + fi + ./ninja.sh +fi + +echo regen >file_a + +${mk} 2> ${log} +if [ -e ninja.sh ]; then + if ! grep regenerating ${log} >/dev/null; then + echo 'Should be regenerated' + fi + ./ninja.sh +fi + +${mk} 2> ${log} +if [ -e ninja.sh ]; then + if grep regenerating ${log}; then + echo 'Should not be regenerated' + fi + ./ninja.sh +fi + +sleep_if_necessary 1 +echo regen >>file_a + +${mk} 2> ${log} +if [ -e ninja.sh ]; then + if ! grep regenerating ${log} >/dev/null; then + echo 'Should be regenerated' + fi + ./ninja.sh +fi diff --git a/testcase/ninja_regen_filefunc_write.sh b/testcase/ninja_regen_filefunc_write.sh new file mode 100755 index 0000000..cb438cc --- /dev/null +++ b/testcase/ninja_regen_filefunc_write.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# +# Copyright 2016 Google Inc. 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. + +set -e + +log=/tmp/log +mk="$@" + +cat <<EOF > Makefile +\$(file >file_a,test) +all: + echo foo +EOF + +${mk} 2> ${log} +if [ -e ninja.sh ]; then + if [ ! -f file_a ]; then + echo 'file_a does not exist' + fi + ./ninja.sh + rm file_a +fi + +${mk} 2> ${log} +if [ -e ninja.sh ]; then + if grep regenerating ${log}; then + echo 'Should not be regenerated' + fi + if [ ! -f file_a ]; then + echo 'file_a does not exist' + fi + ./ninja.sh +fi diff --git a/testcase/phony.mk b/testcase/phony.mk index 59fb911..64a67bb 100644 --- a/testcase/phony.mk +++ b/testcase/phony.mk @@ -11,9 +11,6 @@ baz: foo test1: foo bar baz echo PASS test1 from foo bar baz -# Actually, you can use .PHONY! -test2: .PHONY - test3: touch test4 diff --git a/testcase/readonly_global.sh b/testcase/readonly_global.sh new file mode 100644 index 0000000..33695f2 --- /dev/null +++ b/testcase/readonly_global.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# +# Copyright 2016 Google Inc. 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. + +set -u + +mk="$@" + +function build() { + cat <<EOF > Makefile +FOO $1 bar +.KATI_READONLY $2 FOO +FOO $3 baz +all: +EOF + + echo "Testcase: $1 $2 $3" + if echo "${mk}" | grep -q "^make"; then + # Make doesn't support .KATI_READONLY + echo "Makefile:3: *** cannot assign to readonly variable: FOO" + else + ${mk} 2>&1 && echo "Clean exit" + fi +} + +build "=" "=" "=" +build "=" "+=" "=" +build "=" ":=" "=" + +build "=" ":=" ":=" +build "=" ":=" "+=" + +build ":=" ":=" ":=" +build ":=" ":=" "+=" +build ":=" ":=" "=" diff --git a/testcase/readonly_global_missing.sh b/testcase/readonly_global_missing.sh new file mode 100644 index 0000000..cab86f2 --- /dev/null +++ b/testcase/readonly_global_missing.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# +# Copyright 2016 Google Inc. 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. + +set -u + +mk="$@" + +cat <<EOF > Makefile +all: FOO = bar +.KATI_READONLY = FOO +all: +EOF + +if echo "${mk}" | grep -q "^make"; then + # Make doesn't support .KATI_READONLY + echo "Makefile:2: *** unknown variable: FOO" +else + ${mk} 2>&1 && echo "Clean exit" +fi diff --git a/testcase/readonly_rule.sh b/testcase/readonly_rule.sh new file mode 100644 index 0000000..676d639 --- /dev/null +++ b/testcase/readonly_rule.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# +# Copyright 2016 Google Inc. 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. + +set -u + +mk="$@" + +function build() { + cat <<EOF > Makefile +all: FOO $1 bar +all: .KATI_READONLY $2 FOO +FOO $3 foo +all: FOO $3 baz +all: +EOF + + echo "Testcase: $1 $2 $3" + if echo "${mk}" | grep -q "^make"; then + # Make doesn't support .KATI_READONLY + echo "Makefile:4: *** cannot assign to readonly variable: FOO" + else + ${mk} 2>&1 && echo "Clean exit" + fi +} + +#build "=" "=" "=" +#build "=" "+=" "=" +#build "=" ":=" "=" +# +#build "=" ":=" ":=" +#build "=" ":=" "+=" +# +#build ":=" ":=" ":=" +build ":=" ":=" "+=" +#build ":=" ":=" "=" diff --git a/testcase/readonly_rule_missing.sh b/testcase/readonly_rule_missing.sh new file mode 100644 index 0000000..6940438 --- /dev/null +++ b/testcase/readonly_rule_missing.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# +# Copyright 2016 Google Inc. 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. + +set -u + +mk="$@" + +cat <<EOF > Makefile +FOO = bar +all: .KATI_READONLY = FOO +all: +EOF + +if echo "${mk}" | grep -q "^make"; then + # Make doesn't support .KATI_READONLY + echo "Makefile:2: *** unknown variable: FOO" +else + ${mk} 2>&1 && echo "Clean exit" +fi @@ -37,7 +37,7 @@ const char* GetOriginStr(VarOrigin origin) { return "*** broken origin ***"; } -Var::Var() { +Var::Var() : readonly_(false) { } Var::~Var() { @@ -130,10 +130,15 @@ Var* Vars::Lookup(Symbol name) const { return v; } -void Vars::Assign(Symbol name, Var* v) { +void Vars::Assign(Symbol name, Var* v, bool* readonly) { + *readonly = false; auto p = emplace(name, v); if (!p.second) { Var* orig = p.first->second; + if (orig->ReadOnly()) { + *readonly = true; + return; + } if (orig->Origin() == VarOrigin::OVERRIDE || orig->Origin() == VarOrigin::ENVIRONMENT_OVERRIDE) { return; @@ -56,8 +56,14 @@ class Var : public Evaluable { virtual string DebugString() const = 0; + bool ReadOnly() const { return readonly_; } + void SetReadOnly() { readonly_ = true; } + protected: Var(); + + private: + bool readonly_; }; class SimpleVar : public Var { @@ -177,7 +183,7 @@ class Vars : public unordered_map<Symbol, Var*> { Var* Lookup(Symbol name) const; - void Assign(Symbol name, Var* v); + void Assign(Symbol name, Var* v, bool* readonly); static void add_used_env_vars(Symbol v); |
