aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-05-08 03:31:41 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-05-08 03:31:41 +0000
commit8863034cfb657b02b76889ef3e9470ff34c31af1 (patch)
treebb3ebd2cf181f276d1aeac9483e4f7ea97b5a699
parent97a86efe130f4221e7329ec12856c6dea4ae3f1b (diff)
parent89a026243359a42eac9e8cab5b3ba9f6a54c3ed4 (diff)
downloadkati-8863034cfb657b02b76889ef3e9470ff34c31af1.tar.gz
Snap for 10084189 from 89a026243359a42eac9e8cab5b3ba9f6a54c3ed4 to build-tools-release
Change-Id: If8fd17cc5fd2367236bb17415304d2c42a5f1402
-rw-r--r--Makefile.ckati1
-rw-r--r--src/Android.bp12
-rw-r--r--src/eval.cc2
-rw-r--r--src/file_cache.cc38
-rw-r--r--src/file_cache.h15
-rw-r--r--src/func.cc50
-rw-r--r--src/main.cc15
-rw-r--r--src/parser.cc89
-rw-r--r--src/parser.h6
-rw-r--r--src/regen.cc4
-rw-r--r--src/regen_dump.cc29
-rw-r--r--src/regen_dump.h22
-rw-r--r--testcase/ninja_regen_extra_file_deps.sh63
-rw-r--r--testcase/ninja_regen_extra_file_deps_error_on_missing_file.sh35
-rw-r--r--testcase/ninja_shell_no_rerun.sh55
-rw-r--r--testcase/ninja_shell_no_rerun_error_in_rule.sh39
16 files changed, 364 insertions, 111 deletions
diff --git a/Makefile.ckati b/Makefile.ckati
index 322d747..05cdc0d 100644
--- a/Makefile.ckati
+++ b/Makefile.ckati
@@ -40,6 +40,7 @@ KATI_CXX_SRCS := \
ninja.cc \
parser.cc \
regen.cc \
+ regen_dump.cc \
rule.cc \
stats.cc \
stmt.cc \
diff --git a/src/Android.bp b/src/Android.bp
index c56ab34..f0723f9 100644
--- a/src/Android.bp
+++ b/src/Android.bp
@@ -64,7 +64,10 @@ cc_library_host_static {
cc_binary_host {
name: "ckati",
defaults: ["ckati_defaults"],
- srcs: ["main.cc"],
+ srcs: [
+ "main.cc",
+ "regen_dump.cc",
+ ],
whole_static_libs: ["libckati"],
target: {
linux_glibc: {
@@ -73,13 +76,6 @@ cc_binary_host {
},
}
-cc_binary_host {
- name: "ckati_stamp_dump",
- defaults: ["ckati_defaults"],
- srcs: ["regen_dump.cc"],
- static_libs: ["libckati"],
-}
-
cc_test_host {
name: "ckati_test",
defaults: ["ckati_defaults"],
diff --git a/src/eval.cc b/src/eval.cc
index aacb871..3393586 100644
--- a/src/eval.cc
+++ b/src/eval.cc
@@ -553,7 +553,7 @@ void Evaluator::EvalCommand(const CommandStmt* stmt) {
if (!last_rule_) {
std::vector<Stmt*> stmts;
- ParseNotAfterRule(stmt->orig, stmt->loc(), &stmts);
+ ParseNoStats(stmt->orig, stmt->loc(), &stmts);
for (Stmt* a : stmts)
a->Eval(this);
return;
diff --git a/src/file_cache.cc b/src/file_cache.cc
index d0b4ea5..465c45a 100644
--- a/src/file_cache.cc
+++ b/src/file_cache.cc
@@ -19,30 +19,28 @@
#include "file.h"
#include "file_cache.h"
-MakefileCacheManager::MakefileCacheManager() = default;
-
-MakefileCacheManager::~MakefileCacheManager() = default;
-
-class MakefileCacheManagerImpl : public MakefileCacheManager {
- public:
- virtual const Makefile& ReadMakefile(const std::string& filename) override {
- auto iter = cache_.find(filename);
- if (iter != cache_.end()) {
- return iter->second;
- }
- return (cache_.emplace(filename, filename).first)->second;
+const Makefile& MakefileCacheManager::ReadMakefile(
+ const std::string& filename) {
+ auto iter = cache_.find(filename);
+ if (iter != cache_.end()) {
+ return iter->second;
}
+ return (cache_.emplace(filename, filename).first)->second;
+}
- virtual void GetAllFilenames(std::unordered_set<std::string>* out) override {
- for (const auto& p : cache_)
- out->insert(p.first);
- }
+void MakefileCacheManager::GetAllFilenames(
+ std::unordered_set<std::string>* out) {
+ for (const auto& p : cache_)
+ out->insert(p.first);
+ for (const auto& f : extra_file_deps_)
+ out->insert(f);
+}
- private:
- std::unordered_map<std::string, Makefile> cache_;
-};
+void MakefileCacheManager::AddExtraFileDep(std::string_view dep) {
+ extra_file_deps_.emplace(dep);
+}
MakefileCacheManager& MakefileCacheManager::Get() {
- static MakefileCacheManagerImpl instance;
+ static MakefileCacheManager instance;
return instance;
}
diff --git a/src/file_cache.h b/src/file_cache.h
index ce1cd9c..ce3be6a 100644
--- a/src/file_cache.h
+++ b/src/file_cache.h
@@ -22,15 +22,18 @@ class Makefile;
class MakefileCacheManager {
public:
- virtual ~MakefileCacheManager();
-
- virtual const Makefile& ReadMakefile(const std::string& filename) = 0;
- virtual void GetAllFilenames(std::unordered_set<std::string>* out) = 0;
+ const Makefile& ReadMakefile(const std::string& filename);
+ void GetAllFilenames(std::unordered_set<std::string>* out);
+ void AddExtraFileDep(std::string_view dep);
static MakefileCacheManager& Get();
- protected:
- MakefileCacheManager();
+ private:
+ MakefileCacheManager() = default;
+ MakefileCacheManager(const MakefileCacheManager&) = delete;
+ MakefileCacheManager(MakefileCacheManager&&) = delete;
+ std::unordered_map<std::string, Makefile> cache_;
+ std::unordered_set<std::string> extra_file_deps_;
};
#endif // FILE_CACHE_H_
diff --git a/src/func.cc b/src/func.cc
index 2345808..316e423 100644
--- a/src/func.cc
+++ b/src/func.cc
@@ -30,6 +30,7 @@
#include <unordered_map>
#include "eval.h"
+#include "file_cache.h"
#include "fileutil.h"
#include "find.h"
#include "loc.h"
@@ -608,8 +609,8 @@ void ShellFunc(const std::vector<Value*>& args, Evaluator* ev, std::string* s) {
return;
}
- const std::string&& shell = ev->GetShell();
- const std::string&& shellflag = ev->GetShellFlag();
+ std::string shell = ev->GetShell();
+ std::string shellflag = ev->GetShellFlag();
std::string out;
FindCommand* fc = NULL;
@@ -629,6 +630,32 @@ void ShellFunc(const std::vector<Value*>& args, Evaluator* ev, std::string* s) {
ShellStatusVar::SetValue(returnCode);
}
+void ShellFuncNoRerun(const std::vector<Value*>& args,
+ Evaluator* ev,
+ std::string* s) {
+ std::string cmd = args[0]->Eval(ev);
+ if (ev->avoid_io() && !HasNoIoInShellScript(cmd)) {
+ // In the regular ShellFunc, if it sees a $(shell) inside of a rule when in
+ // ninja mode, the shell command will just be written to the ninja file
+ // instead of run directly by kati. So it already has the benefits of not
+ // rerunning every time kati is invoked.
+ ERROR_LOC(ev->loc(),
+ "KATI_shell_no_rerun provides no benefit over regular $(shell) "
+ "inside of a rule.",
+ cmd.c_str());
+ return;
+ }
+
+ std::string shell = ev->GetShell();
+ std::string shellflag = ev->GetShellFlag();
+
+ std::string out;
+ FindCommand* fc = NULL;
+ int returnCode = ShellFuncImpl(shell, shellflag, cmd, ev->loc(), &out, &fc);
+ *s += out;
+ ShellStatusVar::SetValue(returnCode);
+}
+
void CallFunc(const std::vector<Value*>& args, Evaluator* ev, std::string* s) {
static const Symbol tmpvar_names[] = {
Intern("0"), Intern("1"), Intern("2"), Intern("3"), Intern("4"),
@@ -1008,6 +1035,22 @@ void VariableLocationFunc(const std::vector<Value*>& args,
}
}
+void ExtraFileDepsFunc(const std::vector<Value*>& args,
+ Evaluator* ev,
+ std::string*) {
+ for (auto arg : args) {
+ std::string files = arg->Eval(ev);
+ for (std::string_view file : WordScanner(files)) {
+ if (!Exists(file)) {
+ std::string errorMsg = "*** file does not exist: ";
+ errorMsg += file;
+ ev->Error(errorMsg);
+ }
+ MakefileCacheManager::Get().AddExtraFileDep(file);
+ }
+ }
+}
+
#define ENTRY(name, args...) \
{ \
name, { name, args } \
@@ -1066,6 +1109,9 @@ static const std::unordered_map<std::string_view, FuncInfo> g_func_info_map = {
ENTRY("KATI_profile_makefile", &ProfileFunc, 0, 0, false, false),
ENTRY("KATI_variable_location", &VariableLocationFunc, 1, 1, false, false),
+
+ ENTRY("KATI_extra_file_deps", &ExtraFileDepsFunc, 0, 0, false, false),
+ ENTRY("KATI_shell_no_rerun", &ShellFuncNoRerun, 1, 1, false, false),
};
} // namespace
diff --git a/src/main.cc b/src/main.cc
index a32d013..f6dcc7c 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -37,6 +37,7 @@
#include "ninja.h"
#include "parser.h"
#include "regen.h"
+#include "regen_dump.h"
#include "stats.h"
#include "stmt.h"
#include "stringprintf.h"
@@ -354,9 +355,17 @@ static void HandleRealpath(int argc, char** argv) {
}
int main(int argc, char* argv[]) {
- if (argc >= 2 && !strcmp(argv[1], "--realpath")) {
- HandleRealpath(argc - 2, argv + 2);
- return 0;
+ if (argc >= 2) {
+ if (!strcmp(argv[1], "--realpath")) {
+ HandleRealpath(argc - 2, argv + 2);
+ return 0;
+ } else if (!strcmp(argv[1], "--dump_stamp_tool")) {
+ // Unfortunately, this can easily be confused with --dump_kati_stamp,
+ // which prints debug info about the stamp while executing a normal kati
+ // run. This tool flag only dumps information, and doesn't run the rest of
+ // kati.
+ return stamp_dump_main(argc, argv);
+ }
}
std::string orig_args;
for (int i = 0; i < argc; i++) {
diff --git a/src/parser.cc b/src/parser.cc
index 11da3c7..5f2016d 100644
--- a/src/parser.cc
+++ b/src/parser.cc
@@ -28,12 +28,6 @@
#include "stmt.h"
#include "strutil.h"
-enum struct ParserState {
- NOT_AFTER_RULE = 0,
- AFTER_RULE,
- MAYBE_AFTER_RULE,
-};
-
class Parser {
struct IfState {
IfStmt* stmt;
@@ -48,20 +42,15 @@ class Parser {
public:
Parser(std::string_view buf, const char* filename, std::vector<Stmt*>* stmts)
: buf_(buf),
- state_(ParserState::NOT_AFTER_RULE),
stmts_(stmts),
out_stmts_(stmts),
- num_define_nest_(0),
- num_if_nest_(0),
loc_(filename, 0),
fixed_lineno_(false) {}
Parser(std::string_view buf, const Loc& loc, std::vector<Stmt*>* stmts)
: buf_(buf),
- state_(ParserState::NOT_AFTER_RULE),
stmts_(stmts),
out_stmts_(stmts),
- num_if_nest_(0),
loc_(loc),
fixed_lineno_(true) {}
@@ -93,8 +82,6 @@ class Parser {
"*** missing `endef', unterminated `define'.");
}
- void set_state(ParserState st) { state_ = st; }
-
static std::vector<ParseErrorStmt*> parse_errors;
private:
@@ -127,7 +114,7 @@ class Parser {
current_directive_ = AssignDirective::NONE;
- if (line[0] == '\t' && state_ != ParserState::NOT_AFTER_RULE) {
+ if (line[0] == '\t' && after_rule_) {
CommandStmt* stmt = new CommandStmt();
stmt->set_loc(loc_);
Loc mutable_loc(loc_);
@@ -184,7 +171,6 @@ class Parser {
return;
}
- const bool is_rule = sep != std::string::npos && line[sep] == ':';
RuleStmt* rule_stmt = new RuleStmt();
rule_stmt->set_loc(loc_);
@@ -216,7 +202,7 @@ class Parser {
rule_stmt->rhs = NULL;
}
out_stmts_->push_back(rule_stmt);
- state_ = is_rule ? ParserState::AFTER_RULE : ParserState::MAYBE_AFTER_RULE;
+ after_rule_ = true;
}
void ParseAssign(std::string_view line, size_t separator_pos) {
@@ -249,7 +235,7 @@ class Parser {
stmt->directive = current_directive_;
stmt->is_final = is_final;
out_stmts_->push_back(stmt);
- state_ = ParserState::NOT_AFTER_RULE;
+ after_rule_ = false;
}
void ParseInclude(std::string_view line, std::string_view directive) {
@@ -259,7 +245,7 @@ class Parser {
stmt->expr = ParseExpr(&mutable_loc, line);
stmt->should_exist = directive[0] == 'i';
out_stmts_->push_back(stmt);
- state_ = ParserState::NOT_AFTER_RULE;
+ after_rule_ = false;
}
void ParseDefine(std::string_view line, std::string_view) {
@@ -271,7 +257,7 @@ class Parser {
num_define_nest_ = 1;
define_start_ = 0;
define_start_line_ = loc_.lineno;
- state_ = ParserState::NOT_AFTER_RULE;
+ after_rule_ = false;
}
void ParseInsideDefine(std::string_view line) {
@@ -310,10 +296,10 @@ class Parser {
}
void EnterIf(IfStmt* stmt) {
- IfState* st = new IfState();
- st->stmt = stmt;
- st->is_in_else = false;
- st->num_nest = num_if_nest_;
+ IfState st;
+ st.stmt = stmt;
+ st.is_in_else = false;
+ st.num_nest = num_if_nest_;
if_stack_.push(st);
out_stmts_ = &stmt->true_stmts;
}
@@ -390,19 +376,19 @@ class Parser {
void ParseElse(std::string_view line, std::string_view) {
if (!CheckIfStack("else"))
return;
- IfState* st = if_stack_.top();
- if (st->is_in_else) {
+ IfState& st = if_stack_.top();
+ if (st.is_in_else) {
Error("*** only one `else' per conditional.");
return;
}
- st->is_in_else = true;
- out_stmts_ = &st->stmt->false_stmts;
+ st.is_in_else = true;
+ out_stmts_ = &st.stmt->false_stmts;
std::string_view next_if = TrimLeftSpace(line);
if (next_if.empty())
return;
- num_if_nest_ = st->num_nest + 1;
+ num_if_nest_ = st.num_nest + 1;
if (!HandleDirective(next_if, else_if_directives_)) {
WARN_LOC(loc_, "extraneous text after `else' directive");
}
@@ -416,19 +402,18 @@ class Parser {
Error("extraneous text after `endif` directive");
return;
}
- IfState st = *if_stack_.top();
- for (int t = 0; t <= st.num_nest; t++) {
- delete if_stack_.top();
+ int num_nest = if_stack_.top().num_nest;
+ for (int i = 0; i <= num_nest; i++) {
if_stack_.pop();
- if (if_stack_.empty()) {
- out_stmts_ = stmts_;
- } else {
- IfState* st = if_stack_.top();
- if (st->is_in_else)
- out_stmts_ = &st->stmt->false_stmts;
- else
- out_stmts_ = &st->stmt->true_stmts;
- }
+ }
+ if (if_stack_.empty()) {
+ out_stmts_ = stmts_;
+ } else {
+ const IfState& st = if_stack_.top();
+ if (st.is_in_else)
+ out_stmts_ = &st.stmt->false_stmts;
+ else
+ out_stmts_ = &st.stmt->true_stmts;
}
}
@@ -509,21 +494,24 @@ class Parser {
std::string_view buf_;
size_t l_;
- ParserState state_;
+ // Represents if we just parsed a rule or an expression.
+ // Expressions are included because they can expand into
+ // a rule, see testcase/rule_in_var.mk.
+ bool after_rule_ = false;
std::vector<Stmt*>* stmts_;
std::vector<Stmt*>* out_stmts_;
std::string_view define_name_;
- int num_define_nest_;
+ int num_define_nest_ = 0;
size_t define_start_;
int define_start_line_;
std::string_view orig_line_with_directives_;
AssignDirective current_directive_;
- int num_if_nest_;
- std::stack<IfState*> if_stack_;
+ int num_if_nest_ = 0;
+ std::stack<IfState> if_stack_;
Loc loc_;
bool fixed_lineno_;
@@ -537,8 +525,7 @@ class Parser {
void Parse(Makefile* mk) {
COLLECT_STATS("parse file time");
- Parser parser(std::string_view(mk->buf()), mk->filename().c_str(),
- mk->mutable_stmts());
+ Parser parser(mk->buf(), mk->filename().c_str(), mk->mutable_stmts());
parser.Parse();
}
@@ -546,15 +533,13 @@ void Parse(std::string_view buf,
const Loc& loc,
std::vector<Stmt*>* out_stmts) {
COLLECT_STATS("parse eval time");
- Parser parser(buf, loc, out_stmts);
- parser.Parse();
+ ParseNoStats(buf, loc, out_stmts);
}
-void ParseNotAfterRule(std::string_view buf,
- const Loc& loc,
- std::vector<Stmt*>* out_stmts) {
+void ParseNoStats(std::string_view buf,
+ const Loc& loc,
+ std::vector<Stmt*>* out_stmts) {
Parser parser(buf, loc, out_stmts);
- parser.set_state(ParserState::NOT_AFTER_RULE);
parser.Parse();
}
diff --git a/src/parser.h b/src/parser.h
index d0457d2..21673b3 100644
--- a/src/parser.h
+++ b/src/parser.h
@@ -25,9 +25,9 @@ class Makefile;
void Parse(Makefile* mk);
void Parse(std::string_view buf, const Loc& loc, std::vector<Stmt*>* out_asts);
-void ParseNotAfterRule(std::string_view buf,
- const Loc& loc,
- std::vector<Stmt*>* out_asts);
+void ParseNoStats(std::string_view buf,
+ const Loc& loc,
+ std::vector<Stmt*>* out_asts);
void ParseAssignStatement(std::string_view line,
size_t sep,
diff --git a/src/regen.cc b/src/regen.cc
index 458c4cd..c583556 100644
--- a/src/regen.cc
+++ b/src/regen.cc
@@ -161,7 +161,9 @@ class StampChecker {
for (int i = 0; i < num_files; i++) {
LOAD_STRING(fp, &s);
double ts = GetTimestamp(s);
- if (gen_time < ts) {
+ // GetTimestamp returns < 0 when there's an error reading the file, like
+ // when its been removed.
+ if (gen_time < ts || ts < 0) {
if (g_flags.regen_ignoring_kati_binary) {
if (s == GetExecutablePath()) {
fprintf(stderr, "%s was modified, ignored.\n", s.c_str());
diff --git a/src/regen_dump.cc b/src/regen_dump.cc
index 70034dc..bea1296 100644
--- a/src/regen_dump.cc
+++ b/src/regen_dump.cc
@@ -29,9 +29,7 @@
#include "log.h"
#include "strutil.h"
-using namespace std;
-
-vector<std::string> LoadVecString(FILE* fp) {
+std::vector<std::string> LoadVecString(FILE* fp) {
int count = LoadInt(fp);
if (count < 0) {
ERROR("Incomplete stamp file");
@@ -45,21 +43,22 @@ vector<std::string> LoadVecString(FILE* fp) {
return ret;
}
-int main(int argc, char* argv[]) {
+int stamp_dump_main(int argc, char* argv[]) {
bool dump_files = false;
bool dump_env = false;
bool dump_globs = false;
bool dump_cmds = false;
bool dump_finds = false;
- if (argc == 1) {
- fprintf(stderr,
- "Usage: ckati_stamp_dump [--env] [--files] [--globs] [--cmds] "
- "[--finds] <stamp>\n");
+ if (argc <= 2) {
+ fprintf(
+ stderr,
+ "Usage: ckati --dump_stamp_tool [--env] [--files] [--globs] [--cmds] "
+ "[--finds] <stamp>\n");
return 1;
}
- for (int i = 1; i < argc - 1; i++) {
+ for (int i = 2; i < argc - 1; i++) {
const char* arg = argv[i];
if (!strcmp(arg, "--env")) {
dump_env = true;
@@ -72,7 +71,7 @@ int main(int argc, char* argv[]) {
} else if (!strcmp(arg, "--finds")) {
dump_finds = true;
} else {
- fprintf(stderr, "Unknown option: %s", arg);
+ fprintf(stderr, "Unknown option: %s\n", arg);
return 1;
}
}
@@ -83,7 +82,7 @@ int main(int argc, char* argv[]) {
FILE* fp = fopen(argv[argc - 1], "rb");
if (!fp)
- PERROR("fopen");
+ PERROR(argv[argc - 1]);
ScopedFile sfp(fp);
double gen_time;
@@ -117,8 +116,8 @@ int main(int argc, char* argv[]) {
if (num_envs < 0)
ERROR("Incomplete stamp file");
for (int i = 0; i < num_envs; i++) {
- string name;
- string val;
+ std::string name;
+ std::string val;
if (!LoadString(fp, &name))
ERROR("Incomplete stamp file");
if (!LoadString(fp, &val))
@@ -131,7 +130,7 @@ int main(int argc, char* argv[]) {
if (num_globs < 0)
ERROR("Incomplete stamp file");
for (int i = 0; i < num_globs; i++) {
- string pat;
+ std::string pat;
if (!LoadString(fp, &pat))
ERROR("Incomplete stamp file");
@@ -150,7 +149,7 @@ int main(int argc, char* argv[]) {
ERROR("Incomplete stamp file");
for (int i = 0; i < num_cmds; i++) {
CommandOp op = static_cast<CommandOp>(LoadInt(fp));
- string shell, shellflag, cmd, result, file;
+ std::string shell, shellflag, cmd, result, file;
if (!LoadString(fp, &shell))
ERROR("Incomplete stamp file");
if (!LoadString(fp, &shellflag))
diff --git a/src/regen_dump.h b/src/regen_dump.h
new file mode 100644
index 0000000..c5b4ee5
--- /dev/null
+++ b/src/regen_dump.h
@@ -0,0 +1,22 @@
+// 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.
+
+#ifndef REGEN_DUMP_H_
+#define REGEN_DUMP_H_
+
+#include <string>
+
+int stamp_dump_main(int argc, char* argv[]);
+
+#endif // REGEN_DUMP_H_
diff --git a/testcase/ninja_regen_extra_file_deps.sh b/testcase/ninja_regen_extra_file_deps.sh
new file mode 100644
index 0000000..de21994
--- /dev/null
+++ b/testcase/ninja_regen_extra_file_deps.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+#
+# Copyright 2023 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 -eu
+
+log=stderr_log
+mk="$@"
+
+touch a.txt b.txt
+
+cat <<EOF > Makefile
+EXTRA_DEPS := a.txt b.txt
+\$(KATI_extra_file_deps \$(EXTRA_DEPS))
+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 -q regenerating ${log}; then
+ echo 'Should not be regenerated'
+ fi
+ ./ninja.sh
+fi
+
+touch a.txt
+
+${mk} 2> ${log}
+if [ -e ninja.sh ]; then
+ if ! grep -q regenerating ${log}; then
+ echo 'Should have regenerated due to touched file'
+ fi
+ ./ninja.sh
+fi
+
+rm a.txt
+
+# Ignore the error about a.txt missing on this run, we only care that kati tried to regenerate
+${mk} 2> ${log} || true
+if [ -e ninja.sh ]; then
+ if ! grep -q regenerating ${log}; then
+ echo 'Should have regenerated due to removed file'
+ fi
+ ./ninja.sh
+fi
diff --git a/testcase/ninja_regen_extra_file_deps_error_on_missing_file.sh b/testcase/ninja_regen_extra_file_deps_error_on_missing_file.sh
new file mode 100644
index 0000000..b8e566a
--- /dev/null
+++ b/testcase/ninja_regen_extra_file_deps_error_on_missing_file.sh
@@ -0,0 +1,35 @@
+#!/bin/sh
+#
+# Copyright 2023 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 -eu
+
+log=stderr_log
+mk="$@"
+
+cat <<EOF > Makefile
+\$(KATI_extra_file_deps a.txt)
+all:
+ echo foo
+EOF
+
+${mk} 2> ${log} || true
+if echo "${mk}" | grep -q kati; then
+ if grep -q "file does not exist: a.txt" ${log}; then
+ echo 'foo'
+ else
+ echo 'Expected a missing file error message'
+ fi
+fi
diff --git a/testcase/ninja_shell_no_rerun.sh b/testcase/ninja_shell_no_rerun.sh
new file mode 100644
index 0000000..3b5fd7a
--- /dev/null
+++ b/testcase/ninja_shell_no_rerun.sh
@@ -0,0 +1,55 @@
+#!/bin/sh
+#
+# Copyright 2023 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 -eu
+
+log=stderr_log
+mk="$@"
+
+echo foo > a.txt
+
+if echo "${mk}" | grep -q kati; then
+ cat <<EOF > Makefile
+RESULT := \$(KATI_shell_no_rerun cat a.txt)
+all:
+ echo \$(RESULT)
+EOF
+else
+ cat <<EOF > Makefile
+RESULT := \$(shell cat a.txt)
+all:
+ echo \$(RESULT)
+EOF
+fi
+
+${mk} 2> ${log}
+if [ -e ninja.sh ]; then
+ ./ninja.sh
+fi
+
+# Only change the file for kati so that make matches kati's broken output of printing foo 2 times.
+# ("broken" because the user forgot to add a.txt to $(KATI_extra_file_deps))
+if echo "${mk}" | grep -q kati; then
+echo bar > a.txt
+fi
+
+${mk} 2> ${log}
+if [ -e ninja.sh ]; then
+ if grep -q regenerating ${log}; then
+ echo 'Should not be regenerated'
+ fi
+ ./ninja.sh
+fi
diff --git a/testcase/ninja_shell_no_rerun_error_in_rule.sh b/testcase/ninja_shell_no_rerun_error_in_rule.sh
new file mode 100644
index 0000000..a59c360
--- /dev/null
+++ b/testcase/ninja_shell_no_rerun_error_in_rule.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+#
+# Copyright 2023 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 -eu
+
+log=stderr_log
+mk="$@"
+
+echo foo > a.txt
+
+if echo "${mk}" | grep -q kati; then
+ cat <<EOF > Makefile
+all:
+ echo \$(KATI_shell_no_rerun echo foo)
+EOF
+else
+ cat <<EOF > Makefile
+all:
+ echo foo
+EOF
+fi
+
+${mk} 2> ${log} || true
+if grep -q "KATI_shell_no_rerun provides no benefit over regular \$(shell) inside of a rule" ${log}; then
+ echo foo
+fi