From 977d07a3ae47ef204665d1eda2d642e5064724f3 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Mon, 24 Jun 2019 12:01:19 +0200 Subject: Split build system into library and driver --- build2/algorithm.cxx | 2190 -------------------------------------------------- 1 file changed, 2190 deletions(-) delete mode 100644 build2/algorithm.cxx (limited to 'build2/algorithm.cxx') diff --git a/build2/algorithm.cxx b/build2/algorithm.cxx deleted file mode 100644 index 892767a..0000000 --- a/build2/algorithm.cxx +++ /dev/null @@ -1,2190 +0,0 @@ -// file : build2/algorithm.cxx -*- C++ -*- -// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd -// license : MIT; see accompanying LICENSE file - -#include - -#include -#include -#include -#include // import() -#include -#include -#include -#include -#include - -using namespace std; -using namespace butl; - -namespace build2 -{ - const target& - search (const target& t, const prerequisite_key& pk) - { - assert (phase == run_phase::match); - - // If this is a project-qualified prerequisite, then this is import's - // business. - // - if (pk.proj) - return import (pk); - - if (const target* pt = pk.tk.type->search (t, pk)) - return *pt; - - return create_new_target (pk); - } - - const target* - search_existing (const prerequisite_key& pk) - { - assert (phase == run_phase::match || phase == run_phase::execute); - - return pk.proj ? import_existing (pk) : search_existing_target (pk); - } - - const target& - search (const target& t, name n, const scope& s) - { - assert (phase == run_phase::match); - - auto rp (s.find_target_type (n, location ())); - const target_type* tt (rp.first); - optional& ext (rp.second); - - if (tt == nullptr) - fail << "unknown target type " << n.type << " in name " << n; - - if (!n.dir.empty ()) - n.dir.normalize (false, true); // Current dir collapses to an empty one. - - // @@ OUT: for now we assume the prerequisite's out is undetermined. - // Would need to pass a pair of names. - // - return search (t, - *tt, - n.dir, - dir_path (), - n.value, - ext ? &*ext : nullptr, - &s, - n.proj); - } - - const target* - search_existing (const name& cn, const scope& s, const dir_path& out) - { - assert (phase == run_phase::match || phase == run_phase::execute); - - name n (cn); - auto rp (s.find_target_type (n, location ())); - const target_type* tt (rp.first); - optional& ext (rp.second); - - // For now we treat an unknown target type as an unknown target. Seems - // logical. - // - if (tt == nullptr) - return nullptr; - - if (!n.dir.empty ()) - n.dir.normalize (false, true); // Current dir collapses to an empty one. - - bool q (cn.qualified ()); - - // @@ OUT: for now we assume the prerequisite's out is undetermined. - // Would need to pass a pair of names. - // - prerequisite_key pk { - n.proj, {tt, &n.dir, q ? &empty_dir_path : &out, &n.value, ext}, &s}; - - return q ? import_existing (pk) : search_existing_target (pk); - } - - // target_lock - // -#ifdef __cpp_thread_local - thread_local -#else - __thread -#endif - const target_lock* target_lock::stack = nullptr; - - // If the work_queue is absent, then we don't wait. - // - target_lock - lock_impl (action a, const target& ct, optional wq) - { - assert (phase == run_phase::match); - - // Most likely the target's state is (count_touched - 1), that is, 0 or - // previously executed, so let's start with that. - // - size_t b (target::count_base ()); - size_t e (b + target::offset_touched - 1); - - size_t appl (b + target::offset_applied); - size_t busy (b + target::offset_busy); - - atomic_count& task_count (ct[a].task_count); - - while (!task_count.compare_exchange_strong ( - e, - busy, - memory_order_acq_rel, // Synchronize on success. - memory_order_acquire)) // Synchronize on failure. - { - // Wait for the count to drop below busy if someone is already working - // on this target. - // - if (e >= busy) - { - // Check for dependency cycles. The cycle members should be evident - // from the "while ..." info lines that will follow. - // - if (dependency_cycle (a, ct)) - fail << "dependency cycle detected involving target " << ct; - - if (!wq) - return target_lock {a, nullptr, e - b}; - - // We also unlock the phase for the duration of the wait. Why? - // Consider this scenario: we are trying to match a dir{} target whose - // buildfile still needs to be loaded. Let's say someone else started - // the match before us. So we wait for their completion and they wait - // to switch the phase to load. Which would result in a deadlock - // unless we release the phase. - // - phase_unlock ul; - e = sched.wait (busy - 1, task_count, *wq); - } - - // We don't lock already applied or executed targets. - // - if (e >= appl) - return target_lock {a, nullptr, e - b}; - } - - // We now have the lock. Analyze the old value and decide what to do. - // - target& t (const_cast (ct)); - target::opstate& s (t[a]); - - size_t offset; - if (e <= b) - { - // First lock for this operation. - // - s.rule = nullptr; - s.dependents.store (0, memory_order_release); - - offset = target::offset_touched; - } - else - { - offset = e - b; - assert (offset == target::offset_touched || - offset == target::offset_tried || - offset == target::offset_matched); - } - - return target_lock {a, &t, offset}; - } - - void - unlock_impl (action a, target& t, size_t offset) - { - assert (phase == run_phase::match); - - atomic_count& task_count (t[a].task_count); - - // Set the task count and wake up any threads that might be waiting for - // this target. - // - task_count.store (offset + target::count_base (), memory_order_release); - sched.resume (task_count); - } - - target& - add_adhoc_member (target& t, - const target_type& tt, - const dir_path& dir, - const dir_path& out, - string n) - { - tracer trace ("add_adhoc_member"); - - const_ptr* mp (&t.member); - for (; *mp != nullptr && !(*mp)->is_a (tt); mp = &(*mp)->member) ; - - target& m (*mp != nullptr // Might already be there. - ? **mp - : targets.insert (tt, - dir, - out, - move (n), - nullopt /* ext */, - true /* implied */, - trace).first); - if (*mp == nullptr) - { - *mp = &m; - m.group = &t; - } - - return m; - }; - - // Return the matching rule or NULL if no match and try_match is true. - // - const rule_match* - match_impl (action a, target& t, const rule* skip, bool try_match) - { - // If this is an outer operation (Y-for-X), then we look for rules - // registered for the outer id (X). Note that we still pass the original - // action to the rule's match() function so that it can distinguish - // between a pre/post operation (Y-for-X) and the actual operation (X). - // - meta_operation_id mo (a.meta_operation ()); - operation_id o (a.inner () ? a.operation () : a.outer_operation ()); - - const scope& bs (t.base_scope ()); - - for (auto tt (&t.type ()); tt != nullptr; tt = tt->base) - { - // Search scopes outwards, stopping at the project root. - // - for (const scope* s (&bs); - s != nullptr; - s = s->root () ? global_scope : s->parent_scope ()) - { - const operation_rule_map* om (s->rules[mo]); - - if (om == nullptr) - continue; // No entry for this meta-operation id. - - // First try the map for the actual operation. If that doesn't yeld - // anything, try the wildcard map. - // - for (operation_id oi (o), oip (o); oip != 0; oip = oi, oi = 0) - { - const target_type_rule_map* ttm ((*om)[oi]); - - if (ttm == nullptr) - continue; // No entry for this operation id. - - if (ttm->empty ()) - continue; // Empty map for this operation id. - - auto i (ttm->find (tt)); - - if (i == ttm->end () || i->second.empty ()) - continue; // No rules registered for this target type. - - const auto& rules (i->second); // Hint map. - - // @@ TODO - // - // Different rules can be used for different operations (update vs - // test is a good example). So, at some point, we will probably have - // to support a list of hints or even an operation-hint map (e.g., - // 'hint=cxx test=foo' if cxx supports the test operation but we - // want the foo rule instead). This is also the place where the - // '{build clean}=cxx' construct (which we currently do not support) - // can come handy. - // - // Also, ignore the hint (that is most likely ment for a different - // operation) if this is a unique match. - // - string hint; - auto rs (rules.size () == 1 - ? make_pair (rules.begin (), rules.end ()) - : rules.find_sub (hint)); - - for (auto i (rs.first); i != rs.second; ++i) - { - const auto& r (*i); - const string& n (r.first); - const rule& ru (r.second); - - if (&ru == skip) - continue; - - { - auto df = make_diag_frame ( - [a, &t, &n](const diag_record& dr) - { - if (verb != 0) - dr << info << "while matching rule " << n << " to " - << diag_do (a, t); - }); - - if (!ru.match (a, t, hint)) - continue; - } - - // Do the ambiguity test. - // - bool ambig (false); - - diag_record dr; - for (++i; i != rs.second; ++i) - { - const string& n1 (i->first); - const rule& ru1 (i->second); - - { - auto df = make_diag_frame ( - [a, &t, &n1](const diag_record& dr) - { - if (verb != 0) - dr << info << "while matching rule " << n1 << " to " - << diag_do (a, t); - }); - - // @@ TODO: this makes target state in match() undetermined - // so need to fortify rules that modify anything in match - // to clear things. - // - // @@ Can't we temporarily swap things out in target? - // - if (!ru1.match (a, t, hint)) - continue; - } - - if (!ambig) - { - dr << fail << "multiple rules matching " << diag_doing (a, t) - << info << "rule " << n << " matches"; - ambig = true; - } - - dr << info << "rule " << n1 << " also matches"; - } - - if (!ambig) - return &r; - else - dr << info << "use rule hint to disambiguate this match"; - } - } - } - } - - if (!try_match) - { - diag_record dr; - dr << fail << "no rule to " << diag_do (a, t); - - if (verb < 4) - dr << info << "re-run with --verbose=4 for more information"; - } - - return nullptr; - } - - recipe - apply_impl (action a, - target& t, - const pair>& r) - { - auto df = make_diag_frame ( - [a, &t, &r](const diag_record& dr) - { - if (verb != 0) - dr << info << "while applying rule " << r.first << " to " - << diag_do (a, t); - }); - - return r.second.get ().apply (a, t); - } - - // If step is true then perform only one step of the match/apply sequence. - // - // If try_match is true, then indicate whether there is a rule match with - // the first half of the result. - // - static pair - match_impl (target_lock& l, - bool step = false, - bool try_match = false) - { - assert (l.target != nullptr); - - action a (l.action); - target& t (*l.target); - target::opstate& s (t[a]); - - // Intercept and handle matching an ad hoc group member. - // - if (t.adhoc_member ()) - { - assert (!step); - - const target& g (*t.group); - - // It feels natural to "convert" this call to the one for the group, - // including the try_match part. Semantically, we want to achieve the - // following: - // - // [try_]match (a, g); - // match_recipe (l, group_recipe); - // - auto df = make_diag_frame ( - [a, &t](const diag_record& dr) - { - if (verb != 0) - dr << info << "while matching group rule to " << diag_do (a, t); - }); - - pair r (match (a, g, 0, nullptr, try_match)); - - if (r.first) - { - if (r.second != target_state::failed) - { - match_inc_dependens (a, g); - match_recipe (l, group_recipe); - } - } - else - l.offset = target::offset_tried; - - return r; // Group state. - } - - try - { - // Continue from where the target has been left off. - // - switch (l.offset) - { - case target::offset_tried: - { - if (try_match) - return make_pair (false, target_state::unknown); - - // To issue diagnostics ... - } - // Fall through. - case target::offset_touched: - { - // Match. - // - - // Clear the rule-specific variables, resolved targets list, and the - // data pad before calling match(). The rule is free to modify these - // in its match() (provided that it matches) in order to, for - // example, convey some information to apply(). - // - s.vars.clear (); - t.prerequisite_targets[a].clear (); - if (a.inner ()) t.clear_data (); - - const rule_match* r (match_impl (a, t, nullptr, try_match)); - - assert (l.offset != target::offset_tried); // Should have failed. - - if (r == nullptr) // Not found (try_match == true). - { - l.offset = target::offset_tried; - return make_pair (false, target_state::unknown); - } - - s.rule = r; - l.offset = target::offset_matched; - - if (step) - // Note: s.state is still undetermined. - return make_pair (true, target_state::unknown); - - // Otherwise ... - } - // Fall through. - case target::offset_matched: - { - // Apply. - // - set_recipe (l, apply_impl (a, t, *s.rule)); - l.offset = target::offset_applied; - break; - } - default: - assert (false); - } - } - catch (const failed&) - { - // As a sanity measure clear the target data since it can be incomplete - // or invalid (mark()/unmark() should give you some ideas). - // - s.vars.clear (); - t.prerequisite_targets[a].clear (); - if (a.inner ()) t.clear_data (); - - s.state = target_state::failed; - l.offset = target::offset_applied; - } - - return make_pair (true, s.state); - } - - // If try_match is true, then indicate whether there is a rule match with - // the first half of the result. - // - pair - match (action a, - const target& ct, - size_t start_count, - atomic_count* task_count, - bool try_match) - { - // If we are blocking then work our own queue one task at a time. The - // logic here is that we may have already queued other tasks before this - // one and there is nothing bad (except a potentially deep stack trace) - // about working through them while we wait. On the other hand, we want - // to continue as soon as the lock is available in order not to nest - // things unnecessarily. - // - // That's what we used to do but that proved to be too deadlock-prone. For - // example, we may end up popping the last task which needs a lock that we - // are already holding. A fuzzy feeling is that we need to look for tasks - // (compare their task_counts?) that we can safely work on (though we will - // need to watch out for indirections). So perhaps it's just better to keep - // it simple and create a few extra threads. - // - target_lock l ( - lock_impl (a, - ct, - task_count == nullptr - ? optional (scheduler::work_none) - : nullopt)); - - if (l.target != nullptr) - { - assert (l.offset < target::offset_applied); // Shouldn't lock otherwise. - - if (try_match && l.offset == target::offset_tried) - return make_pair (false, target_state::unknown); - - if (task_count == nullptr) - return match_impl (l, false /* step */, try_match); - - // Pass "disassembled" lock since the scheduler queue doesn't support - // task destruction. - // - target_lock::data ld (l.release ()); - - // Also pass our diagnostics and lock stacks (this is safe since we - // expect the caller to wait for completion before unwinding its stack). - // - if (sched.async (start_count, - *task_count, - [a, try_match] (const diag_frame* ds, - const target_lock* ls, - target& t, size_t offset) - { - // Switch to caller's diag and lock stacks. - // - diag_frame::stack_guard dsg (ds); - target_lock::stack_guard lsg (ls); - - try - { - phase_lock pl (run_phase::match); // Can throw. - { - target_lock l {a, &t, offset}; // Reassemble. - match_impl (l, false /* step */, try_match); - // Unlock within the match phase. - } - } - catch (const failed&) {} // Phase lock failure. - }, - diag_frame::stack, - target_lock::stack, - ref (*ld.target), - ld.offset)) - return make_pair (true, target_state::postponed); // Queued. - - // Matched synchronously, fall through. - } - else - { - // Already applied, executed, or busy. - // - if (l.offset >= target::offset_busy) - return make_pair (true, target_state::busy); - - // Fall through. - } - - return ct.try_matched_state (a, false); - } - - group_view - resolve_members_impl (action a, const target& g, target_lock l) - { - // Note that we will be unlocked if the target is already applied. - // - group_view r; - - // Continue from where the target has been left off. - // - switch (l.offset) - { - case target::offset_touched: - case target::offset_tried: - { - // Match (locked). - // - if (match_impl (l, true).second == target_state::failed) - throw failed (); - - if ((r = g.group_members (a)).members != nullptr) - break; - - // To apply ... - } - // Fall through. - case target::offset_matched: - { - // @@ Doing match without execute messes up our target_count. Does - // not seem like it will be easy to fix (we don't know whether - // someone else will execute this target). - // - // @@ What if we always do match & execute together? After all, - // if a group can be resolved in apply(), then it can be - // resolved in match()! - // - - // Apply (locked). - // - if (match_impl (l, true).second == target_state::failed) - throw failed (); - - if ((r = g.group_members (a)).members != nullptr) - break; - - // Unlock and to execute ... - // - l.unlock (); - } - // Fall through. - case target::offset_applied: - { - // Execute (unlocked). - // - // Note that we use execute_direct() rather than execute() here to - // sidestep the dependents count logic. In this context, this is by - // definition the first attempt to execute this rule (otherwise we - // would have already known the members list) and we really do need - // to execute it now. - // - { - phase_switch ps (run_phase::execute); - execute_direct (a, g); - } - - r = g.group_members (a); - break; - } - } - - return r; - } - - void - resolve_group_impl (action, const target&, target_lock l) - { - match_impl (l, true /* step */, true /* try_match */); - } - - template - static void - match_prerequisite_range (action a, target& t, - R&& r, - const S& ms, - const scope* s) - { - auto& pts (t.prerequisite_targets[a]); - - // Start asynchronous matching of prerequisites. Wait with unlocked phase - // to allow phase switching. - // - wait_guard wg (target::count_busy (), t[a].task_count, true); - - size_t i (pts.size ()); // Index of the first to be added. - for (auto&& p: forward (r)) - { - // Ignore excluded. - // - include_type pi (include (a, t, p)); - - if (!pi) - continue; - - prerequisite_target pt (ms - ? ms (a, t, p, pi) - : prerequisite_target (&search (t, p), pi)); - - if (pt.target == nullptr || (s != nullptr && !pt.target->in (*s))) - continue; - - match_async (a, *pt.target, target::count_busy (), t[a].task_count); - pts.push_back (move (pt)); - } - - wg.wait (); - - // Finish matching all the targets that we have started. - // - for (size_t n (pts.size ()); i != n; ++i) - { - const target& pt (*pts[i]); - match (a, pt); - } - } - - void - match_prerequisites (action a, target& t, - const match_search& ms, - const scope* s) - { - match_prerequisite_range (a, t, group_prerequisites (t), ms, s); - } - - void - match_prerequisite_members (action a, target& t, - const match_search_member& msm, - const scope* s) - { - match_prerequisite_range (a, t, group_prerequisite_members (a, t), msm, s); - } - - template - void - match_members (action a, target& t, T const* ts, size_t n) - { - // Pretty much identical to match_prerequisite_range() except we don't - // search. - // - wait_guard wg (target::count_busy (), t[a].task_count, true); - - for (size_t i (0); i != n; ++i) - { - const target* m (ts[i]); - - if (m == nullptr || marked (m)) - continue; - - match_async (a, *m, target::count_busy (), t[a].task_count); - } - - wg.wait (); - - // Finish matching all the targets that we have started. - // - for (size_t i (0); i != n; ++i) - { - const target* m (ts[i]); - - if (m == nullptr || marked (m)) - continue; - - match (a, *m); - } - } - - // Instantiate only for what we need. - // - template void - match_members (action, target&, - const target* const*, size_t); - - template void - match_members (action, target&, - prerequisite_target const*, size_t); - - const fsdir* - inject_fsdir (action a, target& t, bool parent) - { - tracer trace ("inject_fsdir"); - - // If t is a directory (name is empty), say foo/bar/, then t is bar and - // its parent directory is foo/. - // - const dir_path& d (parent && t.name.empty () ? t.dir.directory () : t.dir); - - const scope& bs (scopes.find (d)); - const scope* rs (bs.root_scope ()); - - // If root scope is NULL, then this can mean that we are out of any - // project or if the directory is in src_root. In both cases we don't - // inject anything unless explicitly requested. - // - // Note that we also used to bail out if this is the root of the - // project. But that proved not to be such a great idea in case of - // subprojects (e.g., tests/). - // - const fsdir* r (nullptr); - if (rs != nullptr && !d.sub (rs->src_path ())) - { - l6 ([&]{trace << d << " for " << t;}); - - // Target is in the out tree, so out directory is empty. - // - r = &search (t, d, dir_path (), string (), nullptr, nullptr); - } - else - { - // See if one was mentioned explicitly. - // - for (const prerequisite& p: group_prerequisites (t)) - { - if (p.is_a ()) - { - const target& pt (search (t, p)); - - if (pt.dir == d) - { - r = &pt.as (); - break; - } - } - } - } - - if (r != nullptr) - { - match (a, *r); - t.prerequisite_targets[a].emplace_back (r); - } - - return r; - } - - // Execute the specified recipe (if any) and the scope operation callbacks - // (if any/applicable) then merge and return the resulting target state. - // - static target_state - execute_recipe (action a, target& t, const recipe& r) - { - target_state ts (target_state::unknown); - - try - { - auto df = make_diag_frame ( - [a, &t](const diag_record& dr) - { - if (verb != 0) - dr << info << "while " << diag_doing (a, t); - }); - - // If this is a dir{} target, see if we have any operation callbacks - // in the corresponding scope. - // - const dir* op_t (t.is_a ()); - const scope* op_s (nullptr); - - using op_iterator = scope::operation_callback_map::const_iterator; - pair op_p; - - if (op_t != nullptr) - { - op_s = &scopes.find (t.dir); - - if (op_s->out_path () == t.dir && !op_s->operation_callbacks.empty ()) - { - op_p = op_s->operation_callbacks.equal_range (a); - - if (op_p.first == op_p.second) - op_s = nullptr; // Ignore. - } - else - op_s = nullptr; // Ignore. - } - - // Pre operations. - // - // Note that here we assume the dir{} target cannot be part of a group - // and as a result we (a) don't try to avoid calling post callbacks in - // case of a group failure and (b) merge the pre and post states with - // the group state. - // - if (op_s != nullptr) - { - for (auto i (op_p.first); i != op_p.second; ++i) - if (const auto& f = i->second.pre) - ts |= f (a, *op_s, *op_t); - } - - // Recipe. - // - ts |= r != nullptr ? r (a, t) : target_state::unchanged; - - // Post operations. - // - if (op_s != nullptr) - { - for (auto i (op_p.first); i != op_p.second; ++i) - if (const auto& f = i->second.post) - ts |= f (a, *op_s, *op_t); - } - - // See the recipe documentation for details on what's going on here. - // Note that if the result is group, then the group's state can be - // failed. - // - switch (t[a].state = ts) - { - case target_state::changed: - case target_state::unchanged: - break; - case target_state::postponed: - ts = t[a].state = target_state::unchanged; - break; - case target_state::group: - ts = (*t.group)[a].state; - break; - default: - assert (false); - } - } - catch (const failed&) - { - ts = t[a].state = target_state::failed; - } - - return ts; - } - - void - update_backlink (const file& f, const path& l, bool changed, backlink_mode m) - { - using mode = backlink_mode; - - const path& p (f.path ()); - dir_path d (l.directory ()); - - // At low verbosity levels we print the command if the target changed or - // the link does not exist (we also treat errors as "not exist" and let - // the link update code below handle it). - // - // Note that in the changed case we print it even if the link is not - // actually updated to signal to the user that the updated out target is - // now available in src. - // - if (verb <= 2) - { - if (changed || !butl::entry_exists (l, - false /* follow_symlinks */, - true /* ignore_errors */)) - { - const char* c (nullptr); - switch (m) - { - case mode::link: - case mode::symbolic: c = verb >= 2 ? "ln -s" : "ln"; break; - case mode::hard: c = "ln"; break; - case mode::copy: - case mode::overwrite: c = l.to_directory () ? "cp -r" : "cp"; break; - } - - // Note: 'ln foo/ bar/' means a different thing. - // - if (verb >= 2) - text << c << ' ' << p.string () << ' ' << l.string (); - else - text << c << ' ' << f << " -> " << d; - } - } - - // What if there is no such subdirectory in src (some like to stash their - // executables in bin/ or some such). The easiest is probably just to - // create it even though we won't be cleaning it up. - // - if (!exists (d)) - mkdir_p (d, 2 /* verbosity */); - - update_backlink (p, l, m); - } - - void - update_backlink (const path& p, const path& l, bool changed, backlink_mode m) - { - // As above but with a slightly different diagnostics. - - using mode = backlink_mode; - - dir_path d (l.directory ()); - - if (verb <= 2) - { - if (changed || !butl::entry_exists (l, - false /* follow_symlinks */, - true /* ignore_errors */)) - { - const char* c (nullptr); - switch (m) - { - case mode::link: - case mode::symbolic: c = verb >= 2 ? "ln -s" : "ln"; break; - case mode::hard: c = "ln"; break; - case mode::copy: - case mode::overwrite: c = l.to_directory () ? "cp -r" : "cp"; break; - } - - if (verb >= 2) - text << c << ' ' << p.string () << ' ' << l.string (); - else - text << c << ' ' << p.string () << " -> " << d; - } - } - - if (!exists (d)) - mkdir_p (d, 2 /* verbosity */); - - update_backlink (p, l, m); - } - - static inline void - try_rmbacklink (const path& l, - backlink_mode m, - bool ie /* ignore_errors */= false) - { - // See also clean_backlink() below. - - using mode = backlink_mode; - - if (l.to_directory ()) - { - switch (m) - { - case mode::link: - case mode::symbolic: - case mode::hard: try_rmsymlink (l, true /* directory */, ie); break; - case mode::copy: try_rmdir_r (path_cast (l), ie); break; - case mode::overwrite: break; - } - } - else - { - // try_rmfile() should work for symbolic and hard file links. - // - switch (m) - { - case mode::link: - case mode::symbolic: - case mode::hard: - case mode::copy: try_rmfile (l, ie); break; - case mode::overwrite: break; - } - } - } - - void - update_backlink (const path& p, const path& l, backlink_mode om) - { - using mode = backlink_mode; - - bool d (l.to_directory ()); - mode m (om); // Keep original mode. - - auto print = [&p, &l, &m, d] () - { - if (verb >= 3) - { - const char* c (nullptr); - switch (m) - { - case mode::link: - case mode::symbolic: c = "ln -sf"; break; - case mode::hard: c = "ln -f"; break; - case mode::copy: - case mode::overwrite: c = d ? "cp -r" : "cp"; break; - } - - text << c << ' ' << p.string () << ' ' << l.string (); - } - }; - - try - { - // Normally will be there. - // - if (!dry_run) - try_rmbacklink (l, m); - - // Skip (ad hoc) targets that don't exist. - // - if (!(d ? dir_exists (p) : file_exists (p))) - return; - - for (; !dry_run; ) // Retry/fallback loop. - try - { - switch (m) - { - case mode::link: - case mode::symbolic: mksymlink (p, l, d); break; - case mode::hard: mkhardlink (p, l, d); break; - case mode::copy: - case mode::overwrite: - { - if (d) - { - // Currently, for a directory, we do a "copy-link": we make the - // target directory and then link each entry (for now this is - // only used to "link" a Windows DLL assembly with only files - // inside). - // - dir_path fr (path_cast (p)); - dir_path to (path_cast (l)); - - try_mkdir (to); - - for (const auto& de: dir_iterator (fr, - false /* ignore_dangling */)) - { - path f (fr / de.path ()); - path t (to / de.path ()); - - update_backlink (f, t, mode::link); - } - } - else - cpfile (p, l, cpflags::overwrite_content); - - break; - } - } - - break; // Success. - } - catch (const system_error& e) - { - // If symlinks not supported, try a hardlink. - // - if (m == mode::link) - { - // Note that we are not guaranteed that the system_error exception - // is of the generic category. - // - int c (e.code ().value ()); - if (e.code ().category () == generic_category () && - (c == ENOSYS || // Not implemented. - c == EPERM)) // Not supported by the filesystem(s). - { - m = mode::hard; - continue; - } - } - - throw; - } - } - catch (const system_error& e) - { - const char* w (nullptr); - switch (m) - { - case mode::link: - case mode::symbolic: w = "symbolic link"; break; - case mode::hard: w = "hard link"; break; - case mode::copy: - case mode::overwrite: w = "copy"; break; - } - - print (); - fail << "unable to make " << w << ' ' << l << ": " << e; - } - - print (); - } - - void - clean_backlink (const path& l, uint16_t v /*verbosity*/, backlink_mode m) - { - // Like try_rmbacklink() but with diagnostics and error handling. - - using mode = backlink_mode; - - if (l.to_directory ()) - { - switch (m) - { - case mode::link: - case mode::symbolic: - case mode::hard: rmsymlink (l, true /* directory */, v); break; - case mode::copy: rmdir_r (path_cast (l), true, v); break; - case mode::overwrite: break; - } - } - else - { - // remfile() should work for symbolic and hard file links. - // - switch (m) - { - case mode::link: - case mode::symbolic: - case mode::hard: - case mode::copy: rmfile (l, v); break; - case mode::overwrite: break; - } - } - } - - // If target/link path are syntactically to a directory, then the backlink - // is assumed to be to a directory, otherwise -- to a file. - // - struct backlink: auto_rm - { - using path_type = build2::path; - - reference_wrapper target; - backlink_mode mode; - - backlink (const path_type& t, path_type&& l, backlink_mode m) - : auto_rm (move (l)), target (t), mode (m) - { - assert (t.to_directory () == path.to_directory ()); - } - - ~backlink () - { - if (active) - { - try_rmbacklink (path, mode, true /* ignore_errors */); - active = false; - } - } - - backlink (backlink&&) = default; - backlink& operator= (backlink&&) = default; - }; - - // Normally (i.e., on sane platforms that don't have things like PDBs, etc) - // there will be just one backlink so optimize for that. - // - using backlinks = small_vector; - - static optional - backlink_test (const target& t, const lookup& l) - { - using mode = backlink_mode; - - optional r; - const string& v (cast (l)); - - if (v == "true") r = mode::link; - else if (v == "symbolic") r = mode::symbolic; - else if (v == "hard") r = mode::hard; - else if (v == "copy") r = mode::copy; - else if (v == "overwrite") r = mode::overwrite; - else if (v != "false") - fail << "invalid backlink variable value '" << v << "' " - << "specified for target " << t; - - return r; - } - - static optional - backlink_test (action a, target& t) - { - // Note: the order of these checks is from the least to most expensive. - - // Only for plain update/clean. - // - if (a.outer () || (a != perform_update_id && a != perform_clean_id)) - return nullopt; - - // Only file-based targets in the out tree can be backlinked. - // - if (!t.out.empty () || !t.is_a ()) - return nullopt; - - // Neither an out-of-project nor in-src configuration can be forwarded. - // - const scope& bs (t.base_scope ()); - const scope* rs (bs.root_scope ()); - if (rs == nullptr || bs.src_path () == bs.out_path ()) - return nullopt; - - // Only for forwarded configurations. - // - if (!cast_false (rs->vars[var_forwarded])) - return nullopt; - - lookup l (t.state[a][var_backlink]); - - // If not found, check for some defaults in the global scope (this does - // not happen automatically since target type/pattern-specific lookup - // stops at the project boundary). - // - if (!l.defined ()) - l = global_scope->find (*var_backlink, t.key ()); - - return l ? backlink_test (t, l) : nullopt; - } - - static backlinks - backlink_collect (action a, target& t, backlink_mode m) - { - using mode = backlink_mode; - - const scope& s (t.base_scope ()); - - backlinks bls; - auto add = [&bls, &s] (const path& p, mode m) - { - bls.emplace_back (p, s.src_path () / p.leaf (s.out_path ()), m); - }; - - // First the target itself. - // - add (t.as ().path (), m); - - // Then ad hoc group file/fsdir members, if any. - // - for (const target* mt (t.member); mt != nullptr; mt = mt->member) - { - const path* p (nullptr); - - if (const file* f = mt->is_a ()) - { - p = &f->path (); - - if (p->empty ()) // The "trust me, it's somewhere" case. - p = nullptr; - } - else if (const fsdir* d = mt->is_a ()) - p = &d->dir; - - if (p != nullptr) - { - // Check for a custom backlink mode for this member. If none, then - // inherit the one from the group (so if the user asked to copy .exe, - // we will also copy .pdb). - // - // Note that we want to avoid group or tt/patter-spec lookup. And - // since this is an ad hoc member (which means it was either declared - // in the buildfile or added by the rule), we assume that the value, - // if any, will be set as a rule-specific variable (since setting it - // as a target-specific wouldn't be MT-safe). @@ Don't think this - // applies to declared ad hoc members. - // - lookup l (mt->state[a].vars[var_backlink]); - - optional bm (l ? backlink_test (*mt, l) : m); - - if (bm) - add (*p, *bm); - } - } - - return bls; - } - - static inline backlinks - backlink_update_pre (action a, target& t, backlink_mode m) - { - return backlink_collect (a, t, m); - } - - static void - backlink_update_post (target& t, target_state ts, backlinks& bls) - { - if (ts == target_state::failed) - return; // Let auto rm clean things up. - - // Make backlinks. - // - for (auto b (bls.begin ()), i (b); i != bls.end (); ++i) - { - const backlink& bl (*i); - - if (i == b) - update_backlink (t.as (), - bl.path, - ts == target_state::changed, - bl.mode); - else - update_backlink (bl.target, bl.path, bl.mode); - } - - // Cancel removal. - // - for (backlink& bl: bls) - bl.cancel (); - } - - static void - backlink_clean_pre (action a, target& t, backlink_mode m) - { - backlinks bls (backlink_collect (a, t, m)); - - for (auto b (bls.begin ()), i (b); i != bls.end (); ++i) - { - // Printing anything at level 1 will probably just add more noise. - // - backlink& bl (*i); - bl.cancel (); - clean_backlink (bl.path, i == b ? 2 : 3 /* verbosity */, bl.mode); - } - } - - static target_state - execute_impl (action a, target& t) - { - target::opstate& s (t[a]); - - assert (s.task_count.load (memory_order_consume) == target::count_busy () - && s.state == target_state::unknown); - - target_state ts; - try - { - // Handle target backlinking to forwarded configurations. - // - // Note that this function will never be called if the recipe is noop - // which is ok since such targets are probably not interesting for - // backlinking. - // - backlinks bls; - optional blm (backlink_test (a, t)); - - if (blm) - { - if (a == perform_update_id) - bls = backlink_update_pre (a, t, *blm); - else - backlink_clean_pre (a, t, *blm); - } - - ts = execute_recipe (a, t, s.recipe); - - if (blm) - { - if (a == perform_update_id) - backlink_update_post (t, ts, bls); - } - } - catch (const failed&) - { - // If we could not backlink the target, then the best way to signal the - // failure seems to be to mark the target as failed. - // - ts = s.state = target_state::failed; - } - - // Decrement the target count (see set_recipe() for details). - // - if (a.inner ()) - { - recipe_function** f (s.recipe.target ()); - if (f == nullptr || *f != &group_action) - target_count.fetch_sub (1, memory_order_relaxed); - } - - // Decrement the task count (to count_executed) and wake up any threads - // that might be waiting for this target. - // - size_t tc (s.task_count.fetch_sub ( - target::offset_busy - target::offset_executed, - memory_order_release)); - assert (tc == target::count_busy ()); - sched.resume (s.task_count); - - return ts; - } - - target_state - execute (action a, - const target& ct, - size_t start_count, - atomic_count* task_count) - { - target& t (const_cast (ct)); // MT-aware. - target::opstate& s (t[a]); - - // Update dependency counts and make sure they are not skew. - // - size_t gd (dependency_count.fetch_sub (1, memory_order_relaxed)); - size_t td (s.dependents.fetch_sub (1, memory_order_release)); - assert (td != 0 && gd != 0); - td--; - - // Handle the "last" execution mode. - // - // This gets interesting when we consider interaction with groups. It seem - // to make sense to treat group members as dependents of the group, so, - // for example, if we try to clean the group via three of its members, - // only the last attempt will actually execute the clean. This means that - // when we match a group member, inside we should also match the group in - // order to increment the dependents count. This seems to be a natural - // requirement: if we are delegating to the group, we need to find a - // recipe for it, just like we would for a prerequisite. - // - // Note that we are also going to treat the group state as postponed. - // This is not a mistake: until we execute the recipe, we want to keep - // returning postponed. And once the recipe is executed, it will reset the - // state to group (see group_action()). To put it another way, the - // execution of this member is postponed, not of the group. - // - // Note also that the target execution is postponed with regards to this - // thread. For other threads the state will still be unknown (until they - // try to execute it). - // - if (current_mode == execution_mode::last && td != 0) - return target_state::postponed; - - // Try to atomically change applied to busy. - // - size_t tc (target::count_applied ()); - - size_t exec (target::count_executed ()); - size_t busy (target::count_busy ()); - - if (s.task_count.compare_exchange_strong ( - tc, - busy, - memory_order_acq_rel, // Synchronize on success. - memory_order_acquire)) // Synchronize on failure. - { - // Handle the noop recipe. - // - if (s.state == target_state::unchanged) - { - // There could still be scope operations. - // - if (t.is_a ()) - execute_recipe (a, t, nullptr /* recipe */); - - s.task_count.store (exec, memory_order_release); - sched.resume (s.task_count); - } - else - { - if (task_count == nullptr) - return execute_impl (a, t); - - // Pass our diagnostics stack (this is safe since we expect the - // caller to wait for completion before unwinding its diag stack). - // - if (sched.async (start_count, - *task_count, - [a] (const diag_frame* ds, target& t) - { - diag_frame::stack_guard dsg (ds); - execute_impl (a, t); - }, - diag_frame::stack, - ref (t))) - return target_state::unknown; // Queued. - - // Executed synchronously, fall through. - } - } - else - { - // Either busy or already executed. - // - if (tc >= busy) return target_state::busy; - else assert (tc == exec); - } - - return t.executed_state (a, false); - } - - target_state - execute_direct (action a, const target& ct) - { - target& t (const_cast (ct)); // MT-aware. - target::opstate& s (t[a]); - - // Similar logic to match() above except we execute synchronously. - // - size_t tc (target::count_applied ()); - - size_t exec (target::count_executed ()); - size_t busy (target::count_busy ()); - - if (s.task_count.compare_exchange_strong ( - tc, - busy, - memory_order_acq_rel, // Synchronize on success. - memory_order_acquire)) // Synchronize on failure. - { - if (s.state == target_state::unknown) - execute_impl (a, t); - else - { - assert (s.state == target_state::unchanged || - s.state == target_state::failed); - - if (s.state == target_state::unchanged) - { - if (t.is_a ()) - execute_recipe (a, t, nullptr /* recipe */); - } - - s.task_count.store (exec, memory_order_release); - sched.resume (s.task_count); - } - } - else - { - // If the target is busy, wait for it. - // - if (tc >= busy) sched.wait (exec, s.task_count, scheduler::work_none); - else assert (tc == exec); - } - - return t.executed_state (a); - } - - static inline void - blank_adhoc_member (const target*&) - { - } - - static inline void - blank_adhoc_member (prerequisite_target& pt) - { - if (pt.adhoc) - pt.target = nullptr; - } - - template - target_state - straight_execute_members (action a, atomic_count& tc, - T ts[], size_t n, size_t p) - { - target_state r (target_state::unchanged); - - // Start asynchronous execution of prerequisites. - // - wait_guard wg (target::count_busy (), tc); - - n += p; - for (size_t i (p); i != n; ++i) - { - const target*& mt (ts[i]); - - if (mt == nullptr) // Skipped. - continue; - - target_state s (execute_async (a, *mt, target::count_busy (), tc)); - - if (s == target_state::postponed) - { - r |= s; - mt = nullptr; - } - } - - wg.wait (); - - // Now all the targets in prerequisite_targets must be either still busy - // or executed and synchronized (and we have blanked out all the postponed - // ones). - // - for (size_t i (p); i != n; ++i) - { - if (ts[i] == nullptr) - continue; - - const target& mt (*ts[i]); - - // If the target is still busy, wait for its completion. - // - const auto& tc (mt[a].task_count); - if (tc.load (memory_order_acquire) >= target::count_busy ()) - sched.wait (target::count_executed (), tc, scheduler::work_none); - - r |= mt.executed_state (a); - - blank_adhoc_member (ts[i]); - } - - return r; - } - - template - target_state - reverse_execute_members (action a, atomic_count& tc, - T ts[], size_t n, size_t p) - { - // Pretty much as straight_execute_members() but in reverse order. - // - target_state r (target_state::unchanged); - - wait_guard wg (target::count_busy (), tc); - - n = p - n; - for (size_t i (p); i != n; ) - { - const target*& mt (ts[--i]); - - if (mt == nullptr) - continue; - - target_state s (execute_async (a, *mt, target::count_busy (), tc)); - - if (s == target_state::postponed) - { - r |= s; - mt = nullptr; - } - } - - wg.wait (); - - for (size_t i (p); i != n; ) - { - if (ts[--i] == nullptr) - continue; - - const target& mt (*ts[i]); - - const auto& tc (mt[a].task_count); - if (tc.load (memory_order_acquire) >= target::count_busy ()) - sched.wait (target::count_executed (), tc, scheduler::work_none); - - r |= mt.executed_state (a); - - blank_adhoc_member (ts[i]); - } - - return r; - } - - // Instantiate only for what we need. - // - template target_state - straight_execute_members ( - action, atomic_count&, const target*[], size_t, size_t); - - template target_state - reverse_execute_members ( - action, atomic_count&, const target*[], size_t, size_t); - - template target_state - straight_execute_members ( - action, atomic_count&, prerequisite_target[], size_t, size_t); - - template target_state - reverse_execute_members ( - action, atomic_count&, prerequisite_target[], size_t, size_t); - - pair, const target*> - execute_prerequisites (const target_type* tt, - action a, const target& t, - const timestamp& mt, const execute_filter& ef, - size_t n) - { - assert (current_mode == execution_mode::first); - - auto& pts (t.prerequisite_targets[a]); - - if (n == 0) - n = pts.size (); - - // Pretty much as straight_execute_members() but hairier. - // - target_state rs (target_state::unchanged); - - wait_guard wg (target::count_busy (), t[a].task_count); - - for (size_t i (0); i != n; ++i) - { - const target*& pt (pts[i]); - - if (pt == nullptr) // Skipped. - continue; - - target_state s ( - execute_async ( - a, *pt, target::count_busy (), t[a].task_count)); - - if (s == target_state::postponed) - { - rs |= s; - pt = nullptr; - } - } - - wg.wait (); - - bool e (mt == timestamp_nonexistent); - const target* rt (tt != nullptr ? nullptr : &t); - - for (size_t i (0); i != n; ++i) - { - prerequisite_target& p (pts[i]); - - if (p == nullptr) - continue; - - const target& pt (*p.target); - - const auto& tc (pt[a].task_count); - if (tc.load (memory_order_acquire) >= target::count_busy ()) - sched.wait (target::count_executed (), tc, scheduler::work_none); - - target_state s (pt.executed_state (a)); - rs |= s; - - // Should we compare the timestamp to this target's? - // - if (!e && (p.adhoc || !ef || ef (pt, i))) - { - // If this is an mtime-based target, then compare timestamps. - // - if (const mtime_target* mpt = pt.is_a ()) - { - timestamp mp (mpt->mtime ()); - - // The same logic as in mtime_target::newer() (but avoids a call to - // state()). - // - if (mt < mp || (mt == mp && s == target_state::changed)) - e = true; - } - else - { - // Otherwise we assume the prerequisite is newer if it was changed. - // - if (s == target_state::changed) - e = true; - } - } - - if (p.adhoc) - p.target = nullptr; // Blank out. - else - { - if (rt == nullptr && pt.is_a (*tt)) - rt = &pt; - } - } - - assert (rt != nullptr); - - return pair, const target*> ( - e ? optional () : rs, - tt != nullptr ? rt : nullptr); - } - - target_state - noop_action (action a, const target& t) - { - text << "noop action triggered for " << diag_doing (a, t); - assert (false); // We shouldn't be called (see set_recipe()). - return target_state::unchanged; - } - - target_state - group_action (action a, const target& t) - { - // If the group is busy, we wait, similar to prerequisites. - // - const target& g (*t.group); - - target_state gs (execute (a, g)); - - if (gs == target_state::busy) - sched.wait (target::count_executed (), - g[a].task_count, - scheduler::work_none); - - // Return target_state::group to signal to execute() that this target's - // state comes from the group (which, BTW, can be failed). - // - // There is just one small problem: if the returned group state is - // postponed, then this means the group hasn't been executed yet. And if - // we return target_state::group, then this means any state queries (see - // executed_state()) will be directed to the target which might still not - // be executed or, worse, is being executed as we query. - // - // So in this case we return target_state::postponed (which will result in - // the member being treated as unchanged). This is how it is done for - // prerequisites and seeing that we've been acting as if the group is our - // prerequisite, there is no reason to deviate (see the recipe return - // value documentation for details). - // - return gs != target_state::postponed ? target_state::group : gs; - } - - target_state - default_action (action a, const target& t) - { - return execute_prerequisites (a, t); - } - - target_state - perform_clean_extra (action a, const file& ft, - const clean_extras& extras, - const clean_adhoc_extras& adhoc_extras) - { - // Clean the extras first and don't print the commands at verbosity level - // below 3. Note the first extra file/directory that actually got removed - // for diagnostics below. - // - // Note that dry-run is taken care of by the filesystem functions. - // - target_state er (target_state::unchanged); - bool ed (false); - path ep; - - auto clean_extra = [&er, &ed, &ep] (const file& f, - const path* fp, - const clean_extras& es) - { - for (const char* e: es) - { - size_t n; - if (e == nullptr || (n = strlen (e)) == 0) - continue; - - path p; - bool d; - - if (path::traits_type::absolute (e)) - { - p = path (e); - d = p.to_directory (); - } - else - { - if ((d = (e[n - 1] == '/'))) - --n; - - if (fp == nullptr) - { - fp = &f.path (); - assert (!fp->empty ()); // Must be assigned. - } - - p = *fp; - for (; *e == '-'; ++e) - p = p.base (); - - p.append (e, n); - } - - target_state r (target_state::unchanged); - - if (d) - { - dir_path dp (path_cast (p)); - - switch (build2::rmdir_r (dp, true, 3)) - { - case rmdir_status::success: - { - r = target_state::changed; - break; - } - case rmdir_status::not_empty: - { - if (verb >= 3) - text << dp << " is current working directory, not removing"; - break; - } - case rmdir_status::not_exist: - break; - } - } - else - { - if (rmfile (p, 3)) - r = target_state::changed; - } - - if (r == target_state::changed && ep.empty ()) - { - ed = d; - ep = move (p); - } - - er |= r; - } - }; - - const path& fp (ft.path ()); - - if (!fp.empty () && !extras.empty ()) - clean_extra (ft, nullptr, extras); - - target_state tr (target_state::unchanged); - - // Check if we were asked not to actually remove the files. The extras are - // tricky: some of them, like depdb should definitely be removed. But - // there could also be those that shouldn't. Currently we only use this - // for auto-generated source code where the only extra file, if any, is - // depdb so for now we treat them as "to remove" but in the future we may - // need to have two lists. - // - bool clean (cast_true (ft[var_clean])); - - // Now clean the ad hoc group file members, if any. - // - for (const target* m (ft.member); m != nullptr; m = m->member) - { - const file* mf (m->is_a ()); - const path* mp (mf != nullptr ? &mf->path () : nullptr); - - if (mf == nullptr || mp->empty ()) - continue; - - if (!adhoc_extras.empty ()) - { - auto i (find_if (adhoc_extras.begin (), - adhoc_extras.end (), - [mf] (const clean_adhoc_extra& e) - { - return mf->is_a (e.type); - })); - - if (i != adhoc_extras.end ()) - clean_extra (*mf, mp, i->extras); - } - - if (!clean) - continue; - - // Make this "primary target" for diagnostics/result purposes if the - // primary target is unreal. - // - if (fp.empty ()) - { - if (rmfile (*mp, *mf)) - tr = target_state::changed; - } - else - { - target_state r (rmfile (*mp, 3) - ? target_state::changed - : target_state::unchanged); - - if (r == target_state::changed && ep.empty ()) - ep = *mp; - - er |= r; - } - } - - // Now clean the primary target and its prerequisited in the reverse order - // of update: first remove the file, then clean the prerequisites. - // - if (clean && !fp.empty () && rmfile (fp, ft)) - tr = target_state::changed; - - // Update timestamp in case there are operations after us that could use - // the information. - // - ft.mtime (timestamp_nonexistent); - - // Clean prerequisites. - // - tr |= reverse_execute_prerequisites (a, ft); - - // Factor the result of removing the extra files into the target state. - // While strictly speaking removing them doesn't change the target state, - // if we don't do this, then we may end up removing the file but still - // saying that everything is clean (e.g., if someone removes the target - // file but leaves the extra laying around). That would be confusing. - // - // What would also be confusing is if we didn't print any commands in - // this case. - // - if (tr != target_state::changed && er == target_state::changed) - { - if (verb > (current_diag_noise ? 0 : 1) && verb < 3) - { - if (ed) - text << "rm -r " << path_cast (ep); - else - text << "rm " << ep; - } - } - - tr |= er; - return tr; - } - - target_state - perform_clean (action a, const target& t) - { - const file& f (t.as ()); - assert (!f.path ().empty ()); - return perform_clean_extra (a, f, {}); - } - - target_state - perform_clean_depdb (action a, const target& t) - { - const file& f (t.as ()); - assert (!f.path ().empty ()); - return perform_clean_extra (a, f, {".d"}); - } - - target_state - perform_clean_group (action a, const target& xg) - { - const mtime_target& g (xg.as ()); - - // Similar logic to perform_clean_extra() above. - // - target_state r (target_state::unchanged); - - if (cast_true (g[var_clean])) - { - for (group_view gv (g.group_members (a)); gv.count != 0; --gv.count) - { - if (const target* m = gv.members[gv.count - 1]) - { - if (rmfile (m->as ().path (), *m)) - r |= target_state::changed; - } - } - } - - g.mtime (timestamp_nonexistent); - - r |= reverse_execute_prerequisites (a, g); - return r; - } - - target_state - perform_clean_group_depdb (action a, const target& g) - { - // The same twisted target state merging logic as in perform_clean_extra(). - // - target_state er (target_state::unchanged); - path ep; - - group_view gv (g.group_members (a)); - if (gv.count != 0) - { - ep = gv.members[0]->as ().path () + ".d"; - - if (rmfile (ep, 3)) - er = target_state::changed; - } - - target_state tr (perform_clean_group (a, g)); - - if (tr != target_state::changed && er == target_state::changed) - { - if (verb > (current_diag_noise ? 0 : 1) && verb < 3) - text << "rm " << ep; - } - - tr |= er; - return tr; - } -} -- cgit v1.1