From cf6b3e34b59ad120111e0c1ead779bbb3a70c38d Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Thu, 12 Mar 2015 15:43:17 +0200 Subject: Implement clean operation --- build/algorithm | 20 ++++- build/algorithm.cxx | 99 ++++++++++++++++++--- build/algorithm.ixx | 27 ++++++ build/algorithm.txx | 5 +- build/b.cxx | 22 ++--- build/buildfile | 2 +- build/cxx/rule | 16 ++-- build/cxx/rule.cxx | 84 +++++++++++++----- build/filesystem | 40 +++++++++ build/filesystem.cxx | 56 ++++++++++++ build/mkdir | 22 ----- build/mkdir.cxx | 21 ----- build/path | 3 +- build/rule | 13 +-- build/rule.cxx | 247 +++++++++++++++++++++++++++++++++++++++------------ build/target | 80 +++++++++++++---- build/target.cxx | 17 +++- 17 files changed, 584 insertions(+), 190 deletions(-) create mode 100644 build/filesystem create mode 100644 build/filesystem.cxx delete mode 100644 build/mkdir delete mode 100644 build/mkdir.cxx diff --git a/build/algorithm b/build/algorithm index 8ce35f8..e4b319a 100644 --- a/build/algorithm +++ b/build/algorithm @@ -31,6 +31,12 @@ namespace build void search_and_match (action, target&); + // As above but ignores (does not match) prerequsites that are not + // in the same or a subdirectory of dir. + // + void + search_and_match (action, target&, const path& dir); + // Execute the action on target, assuming a rule has been matched // and the recipe for this action has been set. // @@ -38,8 +44,10 @@ namespace build execute (action, target&); // The default prerequisite execute implementation. It calls execute() - // for each prerequisite in a loop. Returns target_state::changed - // if any of them were changed and target_state::unchanged otherwise. + // on each non-ignored (NULL target) prerequisite in a loop. Returns + // target_state::changed if any of them were changed and + // target_state::unchanged otherwise. Note that this function can be + // used as a recipe. // target_state execute_prerequisites (action, target&); @@ -53,11 +61,17 @@ namespace build // Another version of the above that does two extra things for the // caller: it determines whether the action needs to be executed on // the target based on the passed timestamp and, if so, finds a - // prerequisite of the specified type (e.g., source file). + // prerequisite of the specified type (e.g., a source file). // template T* execute_prerequisites (action, target&, const timestamp&); + + // Standard perform(clean) action implementation for the file target + // or derived. + // + target_state + perform_clean_file (action, target&); } #include diff --git a/build/algorithm.cxx b/build/algorithm.cxx index 46387b3..cccda49 100644 --- a/build/algorithm.cxx +++ b/build/algorithm.cxx @@ -7,6 +7,7 @@ #include // unique_ptr #include // move #include +#include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include using namespace std; @@ -22,7 +24,7 @@ using namespace std; namespace build { target& - search (prerequisite& p) + search_impl (prerequisite& p) { assert (p.target == nullptr); @@ -149,24 +151,33 @@ namespace build search_and_match (action a, target& t) { for (prerequisite& p: t.prerequisites) + match (a, search (p)); + } + + void + search_and_match (action a, target& t, const path& d) + { + for (prerequisite& p: t.prerequisites) { - if (p.target == nullptr) - search (p); + target& pt (search (p)); - match (a, *p.target); + if (pt.dir.sub (d)) + match (a, pt); + else + p.target = nullptr; // Ignore. } } target_state - execute (action a, target& t) + execute_impl (action a, target& t) { // Implementation with some multi-threading ideas in mind. // - switch (target_state ts = t.state ()) + switch (target_state ts = t.state) { case target_state::unknown: { - t.state (target_state::failed); // So the rule can just throw. + t.state = target_state::failed; // So the rule can just throw. auto g ( make_exception_guard ( @@ -175,12 +186,17 @@ namespace build ts = t.recipe (a) (a, t); assert (ts != target_state::unknown && ts != target_state::failed); - t.state (ts); + + // The recipe may have set the target's state manually. + // + if (t.state == target_state::failed) + t.state = ts; + return ts; } case target_state::unchanged: case target_state::changed: - return ts; + assert (false); // Should have been handled by inline execute(). case target_state::failed: throw failed (); } @@ -193,9 +209,12 @@ namespace build for (const prerequisite& p: t.prerequisites) { - assert (p.target != nullptr); + if (p.target == nullptr) // Skip ignored. + continue; + + target& pt (*p.target); - if (execute (a, *p.target) != target_state::unchanged) + if (execute (a, pt) != target_state::unchanged) ts = target_state::changed; } @@ -209,9 +228,10 @@ namespace build for (const prerequisite& p: t.prerequisites) { - assert (p.target != nullptr); - target& pt (*p.target); + if (p.target == nullptr) // Skip ignored. + continue; + target& pt (*p.target); target_state ts (execute (a, pt)); if (!e) @@ -243,4 +263,57 @@ namespace build return e; } + + target_state + perform_clean_file (action a, target& t) + { + // The reverse order of update: first delete the file, then clean + // prerequisites. + // + file& ft (dynamic_cast (t)); + const path& f (ft.path ()); + + rmfile_status rs; + + // We don't want to print the command if we couldn't delete the + // file because it does not exist (just like we don't print the + // update command if the file is up to date). This makes the + // below code a bit ugly. + // + try + { + rs = try_rmfile (f); + } + catch (const system_error& e) + { + if (verb >= 1) + text << "rm " << f.string (); + else + text << "rm " << t; + + fail << "unable to delete file " << f.string () << ": " << e.what (); + } + + if (rs == rmfile_status::success) + { + if (verb >= 1) + text << "rm " << f.string (); + else + text << "rm " << t; + } + + // Update timestamp in case there are operations after us that + // could use the information. + // + ft.mtime (timestamp_nonexistent); + + // Clean prerequisites. + // + target_state ts (target_state::unchanged); + + if (!t.prerequisites.empty ()) + ts = execute_prerequisites (a, t); + + return rs == rmfile_status::success ? target_state::changed : ts; + } } diff --git a/build/algorithm.ixx b/build/algorithm.ixx index e33566b..340789e 100644 --- a/build/algorithm.ixx +++ b/build/algorithm.ixx @@ -4,6 +4,15 @@ namespace build { + target& + search_impl (prerequisite&); + + inline target& + search (prerequisite& p) + { + return p.target != nullptr ? *p.target : search_impl (p); + } + void match_impl (action, target&); @@ -12,5 +21,23 @@ namespace build { if (!t.recipe (a)) match_impl (a, t); + + t.dependents++; + } + + target_state + execute_impl (action, target&); + + inline target_state + execute (action a, target& t) + { + t.dependents--; + + switch (t.state) + { + case target_state::unchanged: + case target_state::changed: return t.state; + default: return execute_impl (a, t); + } } } diff --git a/build/algorithm.txx b/build/algorithm.txx index 4c9a673..1e22be1 100644 --- a/build/algorithm.txx +++ b/build/algorithm.txx @@ -18,9 +18,10 @@ namespace build for (const prerequisite& p: t.prerequisites) { - assert (p.target != nullptr); - target& pt (*p.target); + if (p.target == nullptr) // Skip ignored. + continue; + target& pt (*p.target); target_state ts (execute (a, pt)); if (!e) diff --git a/build/b.cxx b/build/b.cxx index 499ac10..0764622 100644 --- a/build/b.cxx +++ b/build/b.cxx @@ -515,11 +515,8 @@ main (int argc, char* argv[]) target& t (**i); - if (!t.recipe (act)) - { - level4 ([&]{trace << "matching target " << t;}); - match (act, t); - } + level4 ([&]{trace << "matching target " << t;}); + match (act, t); tgs.push_back (t); } @@ -530,18 +527,9 @@ main (int argc, char* argv[]) // for (target& t: tgs) { - // The target might have already been updated indirectly. We - // still want to inform the user about its status since they - // requested its update explicitly. - // - target_state s (t.state ()); - if (s == target_state::unknown) - { - level4 ([&]{trace << "updating target " << t;}); - s = execute (act, t); - } + level4 ([&]{trace << "updating target " << t;}); - switch (s) + switch (execute (act, t)) { case target_state::unchanged: { @@ -552,7 +540,7 @@ main (int argc, char* argv[]) break; case target_state::failed: //@@ This could probably happen in a parallel build. - case target_state::unknown: + default: assert (false); } } diff --git a/build/buildfile b/build/buildfile index 8f11823..4a3b6b3 100644 --- a/build/buildfile +++ b/build/buildfile @@ -1,3 +1,3 @@ exe{b1}: cxx{b algorithm parser lexer name operation spec scope variable \ target prerequisite rule native context search diagnostics cxx/target \ - cxx/rule process timestamp path utility mkdir dump} + cxx/rule process timestamp path utility filesystem dump} diff --git a/build/cxx/rule b/build/cxx/rule index 1dfb8b5..d2378d2 100644 --- a/build/cxx/rule +++ b/build/cxx/rule @@ -17,36 +17,36 @@ namespace build namespace cxx { // @@ Can't we do match(obj&) and then registration code extracts - // that. And no virtuals. + // that. And no virtuals? // class compile: public rule { public: virtual void* - match (action a, target&, const std::string& hint) const; + match (action, target&, const std::string& hint) const; virtual recipe - apply (action a, target&, void*) const; + apply (action, target&, void*) const; static target_state - update (action a, target&); + perform_update (action, target&); private: void - inject_prerequisites (action a, obj&, const cxx&, scope&) const; + inject_prerequisites (action, obj&, const cxx&, scope&) const; }; class link: public rule { public: virtual void* - match (action a, target&, const std::string& hint) const; + match (action, target&, const std::string& hint) const; virtual recipe - apply (action a, target&, void*) const; + apply (action, target&, void*) const; static target_state - update (action a, target&); + perform_update (action, target&); }; } } diff --git a/build/cxx/rule.cxx b/build/cxx/rule.cxx index 4c93a1a..649067e 100644 --- a/build/cxx/rule.cxx +++ b/build/cxx/rule.cxx @@ -69,17 +69,34 @@ namespace build // Search and match all the existing prerequisites. The injection // code (below) takes care of the ones it is adding. // - search_and_match (a, t); + // When cleaning, ignore prerequisites that are not in the same + // or a subdirectory of ours. + // + switch (a.operation ()) + { + case update_id: search_and_match (a, t); break; + case clean_id: search_and_match (a, t, t.dir); break; + default: assert (false); + } - // Inject additional prerequisites. + // Inject additional prerequisites. For now we only do it for + // update. // - auto& sp (*static_cast (v)); - auto& st (dynamic_cast (*sp.target)); + if (a.operation () == update_id) + { + auto& sp (*static_cast (v)); + auto& st (dynamic_cast (*sp.target)); - if (st.mtime () != timestamp_nonexistent) - inject_prerequisites (a, o, st, sp.scope); + if (st.mtime () != timestamp_nonexistent) + inject_prerequisites (a, o, st, sp.scope); + } - return &update; + switch (a) + { + case perform_update_id: return &perform_update; + case perform_clean_id: return &perform_clean_file; + default: return noop_recipe; + } } // Return the next make prerequisite starting from the specified @@ -215,9 +232,7 @@ namespace build // Resolve to target. // - path_target& t ( - dynamic_cast ( - p.target != nullptr ? *p.target : search (p))); + path_target& t (dynamic_cast (search (p))); // Assign path. // @@ -251,7 +266,7 @@ namespace build } target_state compile:: - update (action a, target& t) + perform_update (action a, target& t) { obj& o (dynamic_cast (t)); cxx* s (execute_prerequisites (a, o, o.mtime ())); @@ -359,7 +374,7 @@ namespace build } // We will only chain a C source if there is also a C++ source or we - // we explicitly told to. + // were explicitly told to. // if (seen_c && !seen_cxx && hint < "cxx") { @@ -397,10 +412,15 @@ namespace build if (p.type.id != typeid (c) && p.type.id != typeid (cxx)) { - if (p.target == nullptr) - search (p); + // The same logic as in search_and_match(). + // + target& pt (search (p)); + + if (a.operation () == clean_id && !pt.dir.sub (e.dir)) + p.target = nullptr; // Ignore. + else + build::match (a, pt); - build::match (a, *p.target); continue; } @@ -449,6 +469,26 @@ namespace build // target& ot (search (op)); + // If we are cleaning, check that this target is in the same or + // a subdirectory of ours. + // + // If it is not, then we are effectively leaving the prerequisites + // half-rewritten (we only rewrite those that we should clean). + // What will happen if, say, after clean we have update? Well, + // update will come and finish the rewrite process (it will even + // reuse op that we have created but then ignored). So all is good. + // + if (a.operation () == clean_id && !ot.dir.sub (e.dir)) + { + // If we shouldn't clean obj{}, then it is fair to assume + // we shouldn't clean cxx{} either (generated source will + // be in the same directory as obj{} and if not, well, go + // and find yourself another build system). + // + p.target = nullptr; // Skip. + continue; + } + // If this target already exists, then it needs to be "compatible" // with what we are doing here. // @@ -461,7 +501,6 @@ namespace build // work out, then we fail, in which case searching and matching // speculatively doesn't really hurt. // - // prerequisite* cp1 (nullptr); for (prerequisite& p: ot.prerequisites) { @@ -488,9 +527,7 @@ namespace build if (cp1 != nullptr) { build::match (a, ot); // Now cp1 should be resolved. - - if (cp.target == nullptr) - search (cp); // Our own prerequisite, so this is ok. + search (cp); // Our own prerequisite, so this is ok. if (cp.target != cp1->target) fail << "synthesized target for prerequisite " << cp @@ -510,11 +547,16 @@ namespace build pr = op; } - return &update; + switch (a) + { + case perform_update_id: return &perform_update; + case perform_clean_id: return &perform_clean_file; + default: return noop_recipe; + } } target_state link:: - update (action a, target& t) + perform_update (action a, target& t) { // @@ Q: // diff --git a/build/filesystem b/build/filesystem new file mode 100644 index 0000000..8fa76c2 --- /dev/null +++ b/build/filesystem @@ -0,0 +1,40 @@ +// file : build/filesystem -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Tools CC +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD_FILESYSTEM +#define BUILD_FILESYSTEM + +#include // mode_t + +#include + +namespace build +{ + // Note that you should probably use the default mode 0777 and let + // the umask mechanism adjust it to the user's preferences. Errors + // are reported by throwing std::system_error. + // + void + mkdir (const path&, mode_t = 0777); + + // Try to remove the directory returning not_exist if it does not + // exist and not_empty if it is not empty. All other errors are + // reported by throwing std::system_error. + // + enum class rmdir_status {success, not_exist, not_empty}; + + rmdir_status + try_rmdir (const path&); + + // Try to remove the file (or symbolic link) returning not_exist if + // it does not exist. All other errors are reported by throwing + // std::system_error. + // + enum class rmfile_status {success, not_exist}; + + rmfile_status + try_rmfile (const path&); +} + +#endif // BUILD_FILESYSTEM diff --git a/build/filesystem.cxx b/build/filesystem.cxx new file mode 100644 index 0000000..75d0283 --- /dev/null +++ b/build/filesystem.cxx @@ -0,0 +1,56 @@ +// file : build/filesystem.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2015 Code Synthesis Tools CC +// license : MIT; see accompanying LICENSE file + +#include + +#include // rmdir(), unlink() +#include // mkdir() + +#include + +using namespace std; + +namespace build +{ + void + mkdir (const path& p, mode_t m) + { + if (::mkdir (p.string ().c_str (), m) != 0) + throw system_error (errno, system_category ()); + } + + rmdir_status + try_rmdir (const path& p) + { + rmdir_status r (rmdir_status::success); + + if (::rmdir (p.string ().c_str ()) != 0) + { + if (errno == ENOENT) + r = rmdir_status::not_exist; + else if (errno == ENOTEMPTY || errno == EEXIST) + r = rmdir_status::not_empty; + else + throw system_error (errno, system_category ()); + } + + return r; + } + + rmfile_status + try_rmfile (const path& p) + { + rmfile_status r (rmfile_status::success); + + if (::unlink (p.string ().c_str ()) != 0) + { + if (errno == ENOENT || errno == ENOTDIR) + r = rmfile_status::not_exist; + else + throw system_error (errno, system_category ()); + } + + return r; + } +} diff --git a/build/mkdir b/build/mkdir deleted file mode 100644 index 62e824b..0000000 --- a/build/mkdir +++ /dev/null @@ -1,22 +0,0 @@ -// file : build/mkdir -*- C++ -*- -// copyright : Copyright (c) 2014-2015 Code Synthesis Tools CC -// license : MIT; see accompanying LICENSE file - -#ifndef BUILD_MKDIR -#define BUILD_MKDIR - -#include // mode_t - -#include - -namespace build -{ - // Note that you should probably use the default mode 0777 and let - // the umask mechanism adjust it to the user's preferences. Errors - // are reported by throwing std::system_error. - // - void - mkdir (const path&, mode_t = 0777); -} - -#endif // BUILD_MKDIR diff --git a/build/mkdir.cxx b/build/mkdir.cxx deleted file mode 100644 index 1fbab03..0000000 --- a/build/mkdir.cxx +++ /dev/null @@ -1,21 +0,0 @@ -// file : build/mkdir.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2015 Code Synthesis Tools CC -// license : MIT; see accompanying LICENSE file - -#include - -#include // mkdir() - -#include - -using namespace std; - -namespace build -{ - void - mkdir (const path& p, mode_t m) - { - if (::mkdir (p.string ().c_str (), m) != 0) - throw system_error (errno, system_category ()); - } -} diff --git a/build/path b/build/path index 550af80..9644b6a 100644 --- a/build/path +++ b/build/path @@ -255,7 +255,8 @@ namespace build // Return true if *this is a sub-path of the specified path (i.e., // the specified path is a prefix). Expects both paths to be - // normalized. + // normalized. Note that this function returns true if the paths + // are equal. // bool sub (const basic_path& p) const diff --git a/build/rule b/build/rule index 2e11191..7501f7d 100644 --- a/build/rule +++ b/build/rule @@ -35,7 +35,8 @@ namespace build extern operation_rule_map rules; extern const target_rule_map* current_rules; // Rules for current operation. - // Fallback rule that check that the path exists. + // Fallback rule that on update verifies that the path exists and is + // not older than any of its prerequisites. // class path_rule: public rule { @@ -47,7 +48,7 @@ namespace build apply (action, target&, void*) const; static target_state - update (action, target&); + perform_update (action, target&); }; class dir_rule: public rule @@ -58,9 +59,6 @@ namespace build virtual recipe apply (action, target&, void*) const; - - static target_state - update (action, target&); }; class fsdir_rule: public rule @@ -73,7 +71,10 @@ namespace build apply (action, target&, void*) const; static target_state - update (action, target&); + perform_update (action, target&); + + static target_state + perform_clean (action, target&); }; } diff --git a/build/rule.cxx b/build/rule.cxx index f193760..2359445 100644 --- a/build/rule.cxx +++ b/build/rule.cxx @@ -10,7 +10,8 @@ #include #include #include -#include +#include +#include using namespace std; @@ -21,52 +22,94 @@ namespace build // path_rule // + // Note that this rule is special. It is the last, fallback rule. If + // it doesn't match, then no other rule can possibly match and we have + // an error. It also cannot be ambigious with any other rule. As a + // result the below implementation bends or ignores quite a few rules + // that normal implementations should follow. So you probably shouldn't + // use it as a guide to implement your own, normal, rules. + // void* path_rule:: match (action a, target& t, const string&) const { - // @@ TODO: - // - // - need to try all the target-type-specific extensions, just - // like search_existing_file(). + // While strictly speaking we should check for the file's existence + // for every action (because that's the condition for us matching), + // for some actions this is clearly a waste. Say, perform_clean: we + // are not doing anything for this action so not checking if the file + // exists seems harmless. What about, say, configure_update? Again, + // whether we match or not, there is nothing to be done for this + // action. And who knows, maybe the file doesn't exist during + // configure_update but will magically appear during perform_update. + // So the overall guideline seems to be this: if we don't do anything + // for the action (other than performing it on the prerequisites), + // then we match. // - path_target& pt (dynamic_cast (t)); - - if (pt.path ().empty ()) + switch (a) { - path p (t.dir / path (pt.name)); + case perform_update_id: + { + // @@ TODO: + // + // - need to try all the target-type-specific extensions, just + // like search_existing_file(). + // + path_target& pt (dynamic_cast (t)); - // @@ TMP: target name as an extension. - // - const string& e (pt.ext != nullptr ? *pt.ext : pt.type ().name); + if (pt.path ().empty ()) + { + path p (t.dir / path (pt.name)); + + // @@ TMP: target name as an extension. + // + const string& e (pt.ext != nullptr ? *pt.ext : pt.type ().name); + + if (!e.empty ()) + { + p += '.'; + p += e; + } - if (!e.empty ()) + // While strictly speaking we shouldn't do this in match(), + // no other rule should ever be ambiguous with the fallback + // one. + // + pt.path (move (p)); + } + + return pt.mtime () != timestamp_nonexistent ? &t : nullptr; + } + default: { - p += '.'; - p += e; + return &t; } - - // While strictly speaking we shouldn't do this in match(), - // no other rule should ever be ambiguous with this fallback - // one. - // - pt.path (move (p)); } - - return pt.mtime () != timestamp_nonexistent ? &t : nullptr; } recipe path_rule:: apply (action a, target& t, void*) const { + // Update triggers the update of this target's prerequisites + // so it would seem natural that we should also trigger their + // cleanup. However, this possibility is rather theoretical + // since such an update would render this target out of date + // which in turn would lead to an error. So until we see a + // real use-case for this functionality, we simply ignore + // the clean operation. + // + if (a.operation () == clean_id) + return noop_recipe; + // Search and match all the prerequisites. // search_and_match (a, t); - return &update; + return a == perform_update_id + ? &perform_update + : t.prerequisites.empty () ? noop_recipe : default_recipe; } target_state path_rule:: - update (action a, target& t) + perform_update (action a, target& t) { // Make sure the target is not older than any of its prerequisites. // @@ -75,7 +118,7 @@ namespace build for (const prerequisite& p: t.prerequisites) { target& pt (*p.target); - target_state ts (update (a, pt)); + target_state ts (execute (a, pt)); // If this is an mtime-based target, then compare timestamps. // @@ -113,17 +156,17 @@ namespace build recipe dir_rule:: apply (action a, target& t, void*) const { - search_and_match (a, t); - return &update; - } - - target_state dir_rule:: - update (action a, target& t) - { - // Return changed if any of our prerequsites were updated and - // unchanged otherwise. + // When cleaning, ignore prerequisites that are not in the same + // or a subdirectory of ours. // - return execute_prerequisites (a, t); + switch (a.operation ()) + { + case update_id: search_and_match (a, t); break; + case clean_id: search_and_match (a, t, t.dir); break; + default: assert (false); + } + + return default_recipe; } // fsdir_rule @@ -137,47 +180,137 @@ namespace build recipe fsdir_rule:: apply (action a, target& t, void*) const { - // Let's not allow any prerequisites for this target since it - // doesn't make much sense. The sole purpose of this target type - // is to create a directory. - // - if (!t.prerequisites.empty ()) - fail << "no prerequisites allowed for target " << t; + switch (a.operation ()) + { + case update_id: + { + search_and_match (a, t); + break; + } + case clean_id: + { + // Ignore prerequisites that are not in the same or a subdirectory + // of ours (if t.dir is foo/bar/, then "we" are bar and our directory + // is foo/). Just meditate on it a bit and you will see the light. + // + search_and_match (a, t, t.dir.root () ? t.dir : t.dir.directory ()); + break; + } + default: + assert (false); + } - return &update; + switch (a) + { + case perform_update_id: return &perform_update; + case perform_clean_id: return &perform_clean; + default: return noop_recipe; + } } target_state fsdir_rule:: - update (action a, target& t) + perform_update (action a, target& t) { - path d (t.dir / path (t.name)); + target_state ts (target_state::unchanged); - // Add the extension back if it was specified. + // First update prerequisites (e.g. create parent directories) + // then create this directory. // - if (t.ext != nullptr) + if (!t.prerequisites.empty ()) + ts = execute_prerequisites (a, t); + + const path& d (t.dir); // Everything is in t.dir. + + if (path_mtime (d) == timestamp_nonexistent) { - d += '.'; - d += *t.ext; + if (verb >= 1) + text << "mkdir " << d.string (); + else + text << "mkdir " << t; + + try + { + mkdir (d); + } + catch (const system_error& e) + { + fail << "unable to create directory " << d.string () << ": " + << e.what (); + } + + ts = target_state::changed; } - if (path_mtime (d) != timestamp_nonexistent) + return ts; + } + + target_state fsdir_rule:: + perform_clean (action a, target& t) + { + // Wait until the last dependent to get an empty directory. + // + if (t.dependents != 0) + { + t.state = target_state::unknown; return target_state::unchanged; + } - if (verb >= 1) - text << "mkdir " << d.string (); - else - text << "mkdir " << t; //@@ Probably only show if [show]? + // The reverse order of update: first delete this directory, + // then clean prerequisites (e.g., delete parent directories). + // + const path& d (t.dir); // Everything is in t.dir. + bool w (d == work); // Don't try to delete working directory. + + rmdir_status rs; + // We don't want to print the command if we couldn't delete the + // directory because it does not exist (just like we don't print + // mkdir if it already exists) or if it is not empty. This makes + // the below code a bit ugly. + // try { - mkdir (d); + rs = !w ? try_rmdir (d) : rmdir_status::not_empty; } catch (const system_error& e) { - fail << "unable to create directory " << d.string () << ": " + if (verb >= 1) + text << "rmdir " << d.string (); + else + text << "rmdir " << t; + + fail << "unable to delete directory " << d.string () << ": " << e.what (); } - return target_state::changed; + switch (rs) + { + case rmdir_status::success: + { + if (verb >= 1) + text << "rmdir " << d.string (); + else + text << "rmdir " << t; + + break; + } + case rmdir_status::not_empty: + { + if (verb >= 1) + text << "directory " << d.string () << " is " + << (w ? "cwd" : "not empty") << ", not removing"; + + break; + } + case rmdir_status::not_exist: + break; + } + + target_state ts (target_state::unchanged); + + if (!t.prerequisites.empty ()) + ts = execute_prerequisites (a, t); + + return rs == rmdir_status::success ? target_state::changed : ts; } } diff --git a/build/target b/build/target index a49c57f..f9aa50a 100644 --- a/build/target +++ b/build/target @@ -9,6 +9,7 @@ #include #include #include // unique_ptr +#include // size_t #include // function, reference_wrapper #include #include @@ -27,12 +28,43 @@ namespace build { class target; + // Target state. + // enum class target_state {unknown, unchanged, changed, failed}; - // Note: should throw rather than returning target_state::failed. + // Recipe. + // + // The returned target state should be either changed or unchanged. + // If there is an error, then the recipe should throw rather than + // returning failed. + // + // The recipe execution protocol is as follows: before executing + // the recipe, the caller sets the target's state to failed. If + // the recipe returns normally and the target's state is still + // failed, then the caller sets it to the returned value. This + // means that the recipe can set the target's state manually to + // some other value. For example, setting it to unknown will + // result in the recipe to be executed again if this target is + // a prerequisite of another target. Note that in this case the + // returned by the recipe value is still used (by the caller) as + // the resulting target state for this execution of the recipe. + // + using recipe_function = target_state (action, target&); + using recipe = std::function; + + // Commonly-used recipes. The default recipe executes the action + // on all the prerequisites in a loop, skipping ignored (see the + // execute_prerequisites() in for details). // - typedef std::function recipe; + extern const recipe empty_recipe; + extern const recipe noop_recipe; + extern const recipe default_recipe; + target_state + noop_recipe_function (action, target&); + + // Target type. + // struct target_type { std::type_index id; @@ -69,29 +101,47 @@ namespace build prerequisites_type prerequisites; public: + target_state state; + + // Number of direct targets that depend on this target in the current + // action. It is incremented during the match phase and then decremented + // during execution, before running the recipe. As a result, the recipe + // can detect the last chance (i.e., last dependent) to execute the + // command ("front-running" vs "back-running" recipes). + // + // Note that setting a new recipe (which happens when we match the rule + // and which in turn is triggered by the first dependent) clears this + // counter. However, if the previous action was the same as the current, + // then the existing recipe is reused. In this case, however, the counter + // should have been decremented to 0 naturally, as part of the previous + // action execution. + // + std::size_t dependents; + + public: typedef build::recipe recipe_type; const recipe_type& - recipe (action_id a) const {return action_ == a ? recipe_ : empty_recipe_;} + recipe (action_id a) const {return action_ == a ? recipe_ : empty_recipe;} void recipe (action_id a, recipe_type r) { assert (action_ != a || !recipe_); action_ = a; - recipe_ = r; + recipe_ = std::move (r); - // Also reset the target state. + // Also reset the target state. If this is a noop recipe, then + // mark the target unchanged so that we don't waste time executing + // the recipe. // - state_ = target_state::unknown; - } + recipe_function** f (recipe_.target ()); + state = (f == nullptr || *f != &noop_recipe_function) + ? target_state::unknown + : target_state::unchanged; - public: - target_state - state () const {return state_;} - - void - state (target_state s) {state_ = s;} + dependents = 0; + } private: target (const target&) = delete; @@ -102,10 +152,8 @@ namespace build static const target_type static_type; private: - action_id action_ {0}; // Action id of this target recipe. + action_id action_ {0}; // Action id of this recipe. recipe_type recipe_; - static const recipe_type empty_recipe_; - target_state state_ {target_state::unknown}; }; std::ostream& diff --git a/build/target.cxx b/build/target.cxx index 5cf131f..15d57ca 100644 --- a/build/target.cxx +++ b/build/target.cxx @@ -7,16 +7,29 @@ #include #include #include +#include // execute_prerequisites() #include using namespace std; namespace build { - // target + // recipe // - const recipe target::empty_recipe_; + const recipe empty_recipe; + const recipe noop_recipe (&noop_recipe_function); + const recipe default_recipe ( + static_cast (&execute_prerequisites)); + + target_state + noop_recipe_function (action, target&) + { + assert (false); // We shouldn't be called, see target::recipe(). + return target_state::unchanged; + } + // target + // ostream& operator<< (ostream& os, const target& t) { -- cgit v1.1