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 --- libbuild2/target.cxx | 1260 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1260 insertions(+) create mode 100644 libbuild2/target.cxx (limited to 'libbuild2/target.cxx') diff --git a/libbuild2/target.cxx b/libbuild2/target.cxx new file mode 100644 index 0000000..c823e85 --- /dev/null +++ b/libbuild2/target.cxx @@ -0,0 +1,1260 @@ +// file : libbuild2/target.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2019 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace butl; + +namespace build2 +{ + // target_type + // + bool target_type:: + is_a_base (const target_type& tt) const + { + for (const target_type* b (base); b != nullptr; b = b->base) + if (*b == tt) + return true; + + return false; + } + + // target_state + // + static const char* const target_state_[] = + { + "unknown", + "unchanged", + "postponed", + "busy", + "changed", + "failed", + "group" + }; + + ostream& + operator<< (ostream& os, target_state ts) + { + return os << target_state_[static_cast (ts)]; + } + + // recipe + // + const recipe empty_recipe; + const recipe noop_recipe (&noop_action); + const recipe default_recipe (&default_action); + const recipe group_recipe (&group_action); + + // target + // + const target::prerequisites_type target::empty_prerequisites_; + + target:: + ~target () + { + clear_data (); + } + + const string& target:: + ext (string v) + { + ulock l (targets.mutex_); + + // Once the extension is set, it is immutable. However, it is possible + // that someone has already "branded" this target with a different + // extension. + // + optional& e (*ext_); + + if (!e) + e = move (v); + else if (*e != v) + { + string o (*e); + l.unlock (); + + fail << "conflicting extensions '" << o << "' and '" << v << "' " + << "for target " << *this; + } + + return *e; + } + + group_view target:: + group_members (action) const + { + assert (false); // Not a group or doesn't expose its members. + return group_view {nullptr, 0}; + } + + const scope& target:: + base_scope () const + { + // If this target is from the src tree, use its out directory to find + // the scope. + // + return scopes.find (out_dir ()); + } + + const scope& target:: + root_scope () const + { + // This is tricky to cache so we do the lookup for now. + // + const scope* r (base_scope ().root_scope ()); + assert (r != nullptr); + return *r; + } + + pair target:: + find_original (const variable& var, bool target_only) const + { + pair r (lookup (), 0); + + ++r.second; + { + auto p (vars.find (var)); + if (p.first != nullptr) + r.first = lookup (*p.first, p.second, vars); + } + + const target* g (nullptr); + + if (!r.first) + { + ++r.second; + + // Skip looking up in the ad hoc group, which is semantically the + // first/primary member. + // + if ((g = group == nullptr + ? nullptr + : group->adhoc_group () ? group->group : group)) + { + auto p (g->vars.find (var)); + if (p.first != nullptr) + r.first = lookup (*p.first, p.second, g->vars); + } + } + + // Delegate to scope's find_original(). + // + if (!r.first) + { + if (!target_only) + { + auto p (base_scope ().find_original ( + var, + &type (), + &name, + g != nullptr ? &g->type () : nullptr, + g != nullptr ? &g->name : nullptr)); + + r.first = move (p.first); + r.second = r.first ? r.second + p.second : p.second; + } + else + r.second = size_t (~0); + } + + return r; + } + + value& target:: + append (const variable& var) + { + // Note: see also prerequisite::append() if changing anything here. + + // Note that here we want the original value without any overrides + // applied. + // + lookup l (find_original (var).first); + + if (l.defined () && l.belongs (*this)) // Existing var in this target. + return vars.modify (l); // Ok since this is original. + + value& r (assign (var)); // NULL. + + if (l.defined ()) + r = *l; // Copy value (and type) from the outer scope. + + return r; + } + + pair target::opstate:: + find_original (const variable& var, bool target_only) const + { + pair r (lookup (), 0); + + ++r.second; + { + auto p (vars.find (var)); + if (p.first != nullptr) + r.first = lookup (*p.first, p.second, vars); + } + + // Delegate to target's find_original(). + // + if (!r.first) + { + auto p (target_->find_original (var, target_only)); + + r.first = move (p.first); + r.second = r.first ? r.second + p.second : p.second; + } + + return r; + } + + optional target:: + split_name (string& v, const location& loc) + { + assert (!v.empty ()); + + // We treat a single trailing dot as "specified no extension", double dots + // as a single trailing dot (that is, an escape sequence which can be + // repeated any number of times; in such cases we naturally assume there + // is no default extension) and triple dots as "unspecified (default) + // extension" (used when the extension in the name is not "ours", for + // example, cxx{foo.test...} for foo.test.cxx). An odd number of dots + // other than one or three is invalid. + // + optional r; + + size_t p; + if (v.back () != '.') + { + if ((p = path::traits_type::find_extension (v)) != string::npos) + r = string (v.c_str () + p + 1); + } + else + { + if ((p = v.find_last_not_of ('.')) == string::npos) + fail (loc) << "invalid target name '" << v << "'"; + + p++; // Position of the first trailing dot. + size_t n (v.size () - p); // Number of the trailing dots. + + if (n == 1) + r = string (); + else if (n == 3) + ; + else if (n % 2 == 0) + { + p += n / 2; // Keep half of the dots. + r = string (); + } + else + fail (loc) << "invalid trailing dot sequence in target name '" + << v << "'"; + } + + if (p != string::npos) + v.resize (p); + + return r; + } + + void target:: + combine_name (string& v, const optional& e, bool de) + { + if (v.back () == '.') + { + assert (e && e->empty ()); + + size_t p (v.find_last_not_of ('.')); + assert (p != string::npos); + + p++; // Position of the first trailing dot. + size_t n (v.size () - p); // Number of the trailing dots. + v.append (n, '.'); // Double them. + } + else if (e) + { + v += '.'; + v += *e; // Empty or not. + } + else if (de) + { + if (path::traits_type::find_extension (v) != string::npos) + v += "..."; + } + } + + // target_set + // + target_set targets; + + const target* target_set:: + find (const target_key& k, tracer& trace) const + { + slock sl (mutex_); + map_type::const_iterator i (map_.find (k)); + + if (i == map_.end ()) + return nullptr; + + const target& t (*i->second); + optional& ext (i->first.ext); + + if (ext != k.ext) + { + ulock ul; // Keep locked for trace. + + if (k.ext) + { + // To update the extension we have to re-lock for exclusive access. + // Between us releasing the shared lock and acquiring unique the + // extension could change and possibly a new target that matches the + // key could be inserted. In this case we simply re-run find (). + // + sl.unlock (); + ul = ulock (mutex_); + + if (ext) // Someone set the extension. + { + ul.unlock (); + return find (k, trace); + } + } + + l5 ([&]{ + diag_record r (trace); + r << "assuming target "; + to_stream (r.os, + target_key {&t.type (), &t.dir, &t.out, &t.name, ext}, + stream_verb_max); // Always print the extension. + r << " is the same as the one with "; + + if (!k.ext) + r << "unspecified extension"; + else if (k.ext->empty ()) + r << "no extension"; + else + r << "extension " << *k.ext; + }); + + if (k.ext) + ext = k.ext; + } + + return &t; + } + + pair target_set:: + insert_locked (const target_type& tt, + dir_path dir, + dir_path out, + string name, + optional ext, + bool implied, + tracer& trace) + { + target_key tk {&tt, &dir, &out, &name, move (ext)}; + target* t (const_cast (find (tk, trace))); + + if (t == nullptr) + { + // We sometimes call insert() even if we expect to find an existing + // target in order to keep the same code (see cc/search_library()). + // + assert (phase != run_phase::execute); + + optional e ( + tt.fixed_extension != nullptr + ? string (tt.fixed_extension (tk, nullptr /* root scope */)) + : move (tk.ext)); + + t = tt.factory (tt, move (dir), move (out), move (name)); + + // Re-lock for exclusive access. In the meantime, someone could have + // inserted this target so emplace() below could return false, in which + // case we proceed pretty much like find() except already under the + // exclusive lock. + // + ulock ul (mutex_); + + auto p (map_.emplace (target_key {&tt, &t->dir, &t->out, &t->name, e}, + unique_ptr (t))); + + map_type::iterator i (p.first); + + if (p.second) + { + t->ext_ = &i->first.ext; + t->implied = implied; + t->state.data[0].target_ = t; + t->state.data[1].target_ = t; + return pair (*t, move (ul)); + } + + // The "tail" of find(). + // + t = i->second.get (); + optional& ext (i->first.ext); + + if (ext != e) + { + l5 ([&]{ + diag_record r (trace); + r << "assuming target "; + to_stream ( + r.os, + target_key {&t->type (), &t->dir, &t->out, &t->name, ext}, + stream_verb_max); // Always print the extension. + r << " is the same as the one with "; + + if (!e) + r << "unspecified extension"; + else if (e->empty ()) + r << "no extension"; + else + r << "extension " << *e; + }); + + if (e) + ext = e; + } + + // Fall through (continue as if the first find() returned this target). + } + + if (!implied) + { + // The implied flag can only be cleared during the load phase. + // + assert (phase == run_phase::load); + + // Clear the implied flag. + // + if (t->implied) + t->implied = false; + } + + return pair (*t, ulock ()); + } + + ostream& + to_stream (ostream& os, const target_key& k, optional osv) + { + stream_verbosity sv (osv ? *osv : stream_verb (os)); + uint16_t dv (sv.path); + uint16_t ev (sv.extension); + + // If the name is empty, then we want to print the last component of the + // directory inside {}, e.g., dir{bar/}, not bar/dir{}. + // + bool n (!k.name->empty ()); + + // Note: relative() returns empty for './'. + // + const dir_path& rd (dv < 1 ? relative (*k.dir) : *k.dir); // Relative. + const dir_path& pd (n ? rd : rd.directory ()); // Parent. + + if (!pd.empty ()) + { + if (dv < 1) + os << diag_relative (pd); + else + os << pd.representation (); + } + + const target_type& tt (*k.type); + + os << tt.name << '{'; + + if (n) + { + os << *k.name; + + // If the extension derivation functions are NULL, then it means this + // target type doesn't use extensions. + // + if (tt.fixed_extension != nullptr || tt.default_extension != nullptr) + { + // For verbosity level 0 we don't print the extension. For 1 we print + // it if there is one. For 2 we print 'foo.?' if it hasn't yet been + // assigned and 'foo.' if it is assigned as "no extension" (empty). + // + if (ev > 0 && (ev > 1 || (k.ext && !k.ext->empty ()))) + { + os << '.' << (k.ext ? *k.ext : "?"); + } + } + else + assert (!k.ext); + } + else + os << (rd.empty () ? dir_path (".") : rd.leaf ()).representation (); + + os << '}'; + + // If this target is from src, print its out. + // + if (!k.out->empty ()) + { + if (dv < 1) + { + // Don't print '@./'. + // + const string& o (diag_relative (*k.out, false)); + + if (!o.empty ()) + os << '@' << o; + } + else + os << '@' << *k.out; + } + + return os; + } + + ostream& + operator<< (ostream& os, const target_key& k) + { + if (auto p = k.type->print) + p (os, k); + else + to_stream (os, k, stream_verb (os)); + + return os; + } + + // mtime_target + // + timestamp mtime_target:: + mtime () const + { + // Figure out from which target we should get the value. + // + const mtime_target* t (this); + + switch (phase) + { + case run_phase::load: break; + case run_phase::match: + { + // Similar logic to matched_state_impl(). + // + const opstate& s (state[action () /* inner */]); + size_t o (s.task_count.load (memory_order_relaxed) - // Synchronized. + target::count_base ()); + + if (o != target::offset_applied && o != target::offset_executed) + break; + } + // Fall through. + case run_phase::execute: + { + if (group_state (action () /* inner */)) + t = &group->as (); + + break; + } + } + + return timestamp (duration (t->mtime_.load (memory_order_consume))); + } + + // path_target + // + const string* path_target:: + derive_extension (bool search, const char* de) + { + // See also search_existing_file() if updating anything here. + + // Should be no default extension if searching. + // + assert (!search || de == nullptr); + + // The target should use extensions and they should not be fixed. + // + assert (de == nullptr || type ().default_extension != nullptr); + + if (const string* p = ext ()) + // Note that returning by reference is now MT-safe since once the + // extension is specified, it is immutable. + // + return p; + else + { + optional e; + + // If the target type has the default extension function then try that + // first. The reason for preferring it over what's been provided by the + // caller is that this function will often use the 'extension' variable + // which the user can use to override extensions. But since we pass the + // provided default extension, the target type can override this logic + // (see the exe{} target type for a use case). + // + if (auto f = type ().default_extension) + e = f (key (), base_scope (), de, search); + + if (!e) + { + if (de != nullptr) + e = de; + else + { + if (search) + return nullptr; + + fail << "no default extension for target " << *this << endf; + } + } + + return &ext (move (*e)); + } + } + + const path& path_target:: + derive_path (const char* de, const char* np, const char* ns) + { + path_type p (dir); + + if (np == nullptr || np[0] == '\0') + p /= name; + else + { + p /= np; + p += name; + } + + if (ns != nullptr) + p += ns; + + return derive_path (move (p), de); + } + + const path& path_target:: + derive_path (path_type p, const char* de) + { + // Derive and add the extension if any. + // + { + const string& e (derive_extension (de)); + + if (!e.empty ()) + { + p += '.'; + p += e; + } + } + + path (move (p)); + return path_; + } + + // Search functions. + // + + const target* + target_search (const target&, const prerequisite_key& pk) + { + // The default behavior is to look for an existing target in the + // prerequisite's directory scope. + // + return search_existing_target (pk); + } + + const target* + file_search (const target&, const prerequisite_key& pk) + { + // First see if there is an existing target. + // + if (const target* t = search_existing_target (pk)) + return t; + + // Then look for an existing file in the src tree. + // + return search_existing_file (pk); + } + + void + target_print_0_ext_verb (ostream& os, const target_key& k) + { + stream_verbosity sv (stream_verb (os)); + if (sv.extension == 1) sv.extension = 0; // Remap 1 to 0. + to_stream (os, k, sv); + } + + void + target_print_1_ext_verb (ostream& os, const target_key& k) + { + stream_verbosity sv (stream_verb (os)); + if (sv.extension == 0) sv.extension = 1; // Remap 0 to 1. + to_stream (os, k, sv); + } + + // type info + // + + const target_type target::static_type + { + "target", + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + &target_search, + false + }; + + const target_type mtime_target::static_type + { + "mtime_target", + &target::static_type, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + &target_search, + false + }; + + const target_type path_target::static_type + { + "path_target", + &mtime_target::static_type, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + &target_search, + false + }; + + extern const char file_ext_def[] = ""; + + const target_type file::static_type + { + "file", + &path_target::static_type, + &target_factory, + &target_extension_fix, + nullptr, /* default_extension */ + nullptr, /* pattern */ + &target_print_1_ext_verb, // Print extension even at verbosity level 0. + &file_search, + false + }; + + static const target* + alias_search (const target&, const prerequisite_key& pk) + { + // For an alias we don't want to silently create a target since it will do + // nothing and it most likely not what the user intended. + // + const target* t (search_existing_target (pk)); + + if (t == nullptr || t->implied) + fail << "no explicit target for " << pk; + + return t; + } + + const target_type alias::static_type + { + "alias", + &target::static_type, + &target_factory, + nullptr, // Extension not used. + nullptr, + nullptr, + nullptr, + &alias_search, + false + }; + + // dir + // + bool dir:: + check_implied (const scope& rs, const dir_path& d) + { + try + { + for (const dir_entry& e: dir_iterator (d, true /* ignore_dangling */)) + { + switch (e.type ()) + { + case entry_type::directory: + { + if (check_implied (rs, d / path_cast (e.path ()))) + return true; + + break; + } + case entry_type::regular: + { + if (e.path () == rs.root_extra->buildfile_file) + return true; + + break; + } + default: + break; + } + } + } + catch (const system_error& e) + { + fail << "unable to iterate over " << d << ": " << e << endf; + } + + return false; + } + + prerequisites dir:: + collect_implied (const scope& bs) + { + prerequisites_type r; + const dir_path& d (bs.src_path ()); + + try + { + for (const dir_entry& e: dir_iterator (d, true /* ignore_dangling */)) + { + if (e.type () == entry_type::directory) + r.push_back ( + prerequisite (nullopt, + dir::static_type, + dir_path (e.path ().representation ()), // Relative. + dir_path (), // In the out tree. + string (), + nullopt, + bs)); + } + } + catch (const system_error& e) + { + fail << "unable to iterate over " << d << ": " << e; + } + + return r; + } + + static const target* + dir_search (const target&, const prerequisite_key& pk) + { + tracer trace ("dir_search"); + + // The first step is like in search_alias(): looks for an existing target. + // + const target* t (search_existing_target (pk)); + + if (t != nullptr && !t->implied) + return t; + + // If not found (or is implied), then try to load the corresponding + // buildfile (which would normally define this target). Failed that, see + // if we can assume an implied buildfile which would be equivalent to: + // + // ./: */ + // + const dir_path& d (*pk.tk.dir); + + // We only do this for relative paths. + // + if (d.relative ()) + { + // Note: this code is a custom version of parser::parse_include(). + + const scope& s (*pk.scope); + + // Calculate the new out_base. + // + dir_path out_base (s.out_path () / d); + out_base.normalize (); + + // In our world modifications to the scope structure during search & + // match should be "pure append" in the sense that they should not + // affect any existing targets that have already been searched & + // matched. + // + // A straightforward way to enforce this is to not allow any existing + // targets to be inside any newly created scopes (except, perhaps for + // the directory target itself which we know hasn't been searched yet). + // This, however, is not that straightforward to implement: we would + // need to keep a directory prefix map for all the targets (e.g., in + // target_set). Also, a buildfile could load from a directory that is + // not a subdirectory of out_base. So for now we just assume that this + // is so. And so it is. + // + bool retest (false); + + assert (phase == run_phase::match); + { + // Switch the phase to load. + // + phase_switch ps (run_phase::load); + + // This is subtle: while we were fussing around another thread may + // have loaded the buildfile. So re-test now that we are in exclusive + // phase. + // + if (t == nullptr) + t = search_existing_target (pk); + + if (t != nullptr && !t->implied) + retest = true; + else + { + // Ok, no luck, switch the scope. + // + pair sp ( + switch_scope (*s.rw ().root_scope (), out_base)); + + if (sp.second != nullptr) // Ignore scopes out of any project. + { + scope& base (sp.first); + scope& root (*sp.second); + + const dir_path& src_base (base.src_path ()); + + path bf (src_base / root.root_extra->buildfile_file); + + if (exists (bf)) + { + l5 ([&]{trace << "loading buildfile " << bf << " for " << pk;}); + retest = source_once (root, base, bf, root); + } + else if (exists (src_base)) + { + t = dir::search_implied (base, pk, trace); + retest = (t != nullptr); + } + } + } + } + assert (phase == run_phase::match); + + // If we loaded/implied the buildfile, examine the target again. + // + if (retest) + { + if (t == nullptr) + t = search_existing_target (pk); + + if (t != nullptr && !t->implied) + return t; + } + } + + fail << "no explicit target for " << pk << endf; + } + + static bool + dir_pattern (const target_type&, + const scope&, + string& v, + optional&, + const location&, + bool r) + { + // Add/strip trailing directory separator unless already there. + // + bool d (path::traits_type::is_separator (v.back ())); + + if (r) + { + assert (d); + v.resize (v.size () - 1); + } + else if (!d) + { + v += path::traits_type::directory_separator; + return true; + } + + return false; + } + + const target_type dir::static_type + { + "dir", + &alias::static_type, + &target_factory, + nullptr, // Extension not used. + nullptr, + &dir_pattern, + nullptr, + &dir_search, + false + }; + + const target_type fsdir::static_type + { + "fsdir", + &target::static_type, + &target_factory, + nullptr, // Extension not used. + nullptr, + &dir_pattern, + nullptr, + &target_search, + false + }; + + static optional + exe_target_extension (const target_key&, + const scope&, + const char* e, + bool search) + { + // If we are searching for an executable that is not a target, then use + // the build machine executable extension. Otherwise, if this is a target, + // then we expect the rule to supply the target machine extension. But if + // it doesn't, then fallback to no extension (e.g., a script). + // + return string (!search + ? (e != nullptr ? e : "") + : +#ifdef _WIN32 + "exe" +#else + "" +#endif + ); + } + +#ifdef _WIN32 + static bool + exe_target_pattern (const target_type&, + const scope&, + string& v, + optional& e, + const location& l, + bool r) + { + if (r) + { + assert (e); + e = nullopt; + } + else + { + e = target::split_name (v, l); + + if (!e) + { + e = "exe"; + return true; + } + } + + return false; + } +#endif + + const target_type exe::static_type + { + "exe", + &file::static_type, + &target_factory, + nullptr, /* fixed_extension */ + &exe_target_extension, +#ifdef _WIN32 + &exe_target_pattern, +#else + nullptr, +#endif + nullptr, + &file_search, + false + }; + + static const char* + buildfile_target_extension (const target_key& tk, const scope* root) + { + // If the name is the special 'buildfile', then there is no extension, + // otherwise it is 'build' (or 'build2file' and 'build2' in the + // alternative naming scheme). + + // Let's try hard not to need the root scope by trusting the extensions + // we were given. + // + // BTW, one way to get rid of all this root scope complication is to + // always require explicit extension specification for buildfiles. Since + // they are hardly ever mentioned explicitly, this should probably be ok. + // + if (tk.ext) + return tk.ext->c_str (); + + if (root == nullptr) + { + // The same login as in target::root_scope(). + // + // Note: we are guaranteed the scope is never NULL for prerequisites + // (where out/dir could be relative and none of this will work). + // + root = scopes.find (tk.out->empty () ? *tk.dir : *tk.out).root_scope (); + + if (root == nullptr || root->root_extra == nullptr) + fail << "unable to determine extension for buildfile target " << tk; + } + + return *tk.name == root->root_extra->buildfile_file.string () + ? "" + : root->root_extra->build_ext.c_str (); + } + + static bool + buildfile_target_pattern (const target_type&, + const scope& base, + string& v, + optional& e, + const location& l, + bool r) + { + if (r) + { + assert (e); + e = nullopt; + } + else + { + e = target::split_name (v, l); + + if (!e) + { + const scope* root (base.root_scope ()); + + if (root == nullptr || root->root_extra == nullptr) + fail (l) << "unable to determine extension for buildfile pattern"; + + if (v != root->root_extra->buildfile_file.string ()) + { + e = root->root_extra->build_ext; + return true; + } + } + } + + return false; + } + + const target_type buildfile::static_type + { + "build", + &file::static_type, + &target_factory, + &buildfile_target_extension, + nullptr, /* default_extension */ + &buildfile_target_pattern, + nullptr, + &file_search, + false + }; + + const target_type doc::static_type + { + "doc", + &file::static_type, + &target_factory, + &target_extension_fix, // Same as file (no extension). + nullptr, /* default_extension */ + nullptr, /* pattern */ // Same as file. + &target_print_1_ext_verb, // Same as file. + &file_search, + false + }; + + static const char* + man_extension (const target_key& tk, const scope*) + { + if (!tk.ext) + fail << "man target " << tk << " must include extension (man section)"; + + return tk.ext->c_str (); + } + + const target_type man::static_type + { + "man", + &doc::static_type, + &target_factory, + &man_extension, // Should be specified explicitly. + nullptr, /* default_extension */ + nullptr, + &target_print_1_ext_verb, // Print extension even at verbosity level 0. + &file_search, + false + }; + + extern const char man1_ext[] = "1"; // VC14 rejects constexpr. + + const target_type man1::static_type + { + "man1", + &man::static_type, + &target_factory, + &target_extension_fix, + nullptr, /* default_extension */ + &target_pattern_fix, + &target_print_0_ext_verb, // Fixed extension, no use printing. + &file_search, + false + }; + + static const char* + manifest_target_extension (const target_key& tk, const scope*) + { + // If the name is special 'manifest', then there is no extension, + // otherwise it is .manifest. + // + return *tk.name == "manifest" ? "" : "manifest"; + } + + static bool + manifest_target_pattern (const target_type&, + const scope&, + string& v, + optional& e, + const location& l, + bool r) + { + if (r) + { + assert (e); + e = nullopt; + } + else + { + e = target::split_name (v, l); + + if (!e && v != "manifest") + { + e = "manifest"; + return true; + } + } + + return false; + } + + const target_type manifest::static_type + { + "manifest", + &doc::static_type, + &target_factory, + &manifest_target_extension, + nullptr, /* default_extension */ + &manifest_target_pattern, + nullptr, + &file_search, + false + }; +} -- cgit v1.1