From 593fd960891027b97567b2622ed4b6c16070ab36 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Fri, 28 Apr 2017 08:33:42 +0200 Subject: Implement support for pre-processing version headers (or other files) Also implement the build system version check. --- build2/algorithm | 31 ++-- build2/algorithm.cxx | 7 +- build2/algorithm.ixx | 22 ++- build2/cc/compile.cxx | 31 ++-- build2/cli/rule.cxx | 28 +-- build2/target | 13 ++ build2/utility | 8 +- build2/utility.cxx | 2 + build2/version-impl | 2 +- build2/version/init.cxx | 78 +++++++- build2/version/module | 12 +- build2/version/rule | 17 ++ build2/version/rule.cxx | 474 +++++++++++++++++++++++++++++++++++++++++++++--- 13 files changed, 629 insertions(+), 96 deletions(-) (limited to 'build2') diff --git a/build2/algorithm b/build2/algorithm index da38f19..601d2ef 100644 --- a/build2/algorithm +++ b/build2/algorithm @@ -300,46 +300,47 @@ namespace build2 // which ones should be used for timestamp comparison. If the filter is // NULL, then all the prerequisites are used. // - // Note that the return value is a pair with the second half indicating - // whether any prerequisites were updated. This is used to handle the - // situation where some prerequisites were updated but no update of the - // target is necessary. In this case we still signal that the target was - // (conceptually, but not physically) changed. This is important both to - // propagate the fact that some work has been done and to also allow our - // dependents to detect this case if they are up to something tricky (like - // recursively linking liba{} prerequisites). + // Note that the return value is an optional target state. If the target + // needs updating, then the value absent. Otherwise it is the state that + // should be returned. This is used to handle the situation where some + // prerequisites were updated but no update of the target is necessary. In + // this case we still signal that the target was (conceptually, but not + // physically) changed. This is important both to propagate the fact that + // some work has been done and to also allow our dependents to detect this + // case if they are up to something tricky (like recursively linking liba{} + // prerequisites). // // Note that because we use mtime, this function should normally only be // used in the perform_update action (which is straight). // using prerequisite_filter = function; - pair + optional execute_prerequisites (action, const target&, const timestamp&, const prerequisite_filter& = nullptr); // 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., a source file). If there are multiple prerequisites of this - // type, then the first is returned (this can become important if additional + // on the passed timestamp and finds a prerequisite of the specified type + // (e.g., a source file). If there are multiple prerequisites of this type, + // then the first is returned (this can become important if additional // prerequisites of the same type may get injected). // template - pair + pair, const T&> execute_prerequisites (action, const target&, const timestamp&, const prerequisite_filter& = nullptr); - pair + pair, const target&> execute_prerequisites (const target_type&, action, const target&, const timestamp&, const prerequisite_filter& = nullptr); template - pair + pair, const T&> execute_prerequisites (const target_type&, action, const target&, const timestamp&, diff --git a/build2/algorithm.cxx b/build2/algorithm.cxx index 049df0c..a6208d4 100644 --- a/build2/algorithm.cxx +++ b/build2/algorithm.cxx @@ -1059,7 +1059,7 @@ namespace build2 return r; } - pair + pair, const target*> execute_prerequisites (const target_type* tt, action a, const target& t, const timestamp& mt, const prerequisite_filter& pf) @@ -1141,7 +1141,10 @@ namespace build2 } assert (rt != nullptr); - return make_pair (e ? rt : nullptr, rs); + + return pair, const target*> ( + e ? optional () : rs, + tt != nullptr ? rt : nullptr); } target_state diff --git a/build2/algorithm.ixx b/build2/algorithm.ixx index d39bf9c..46dece4 100644 --- a/build2/algorithm.ixx +++ b/build2/algorithm.ixx @@ -361,44 +361,46 @@ namespace build2 // If the first argument is NULL, then the result is treated as a boolean // value. // - pair + pair, const target*> execute_prerequisites (const target_type*, action, const target&, const timestamp&, const prerequisite_filter&); - inline pair + inline optional execute_prerequisites (action a, const target& t, const timestamp& mt, const prerequisite_filter& pf) { - auto p (execute_prerequisites (nullptr, a, t, mt, pf)); - return make_pair (p.first != nullptr, p.second); + return execute_prerequisites (nullptr, a, t, mt, pf).first; } template - inline pair + inline pair, const T&> execute_prerequisites (action a, const target& t, const timestamp& mt, const prerequisite_filter& pf) { auto p (execute_prerequisites (T::static_type, a, t, mt, pf)); - return make_pair (static_cast (p.first), p.second); + return pair, const T&> ( + p.first, static_cast (p.second)); } - inline pair + inline pair, const target&> execute_prerequisites (const target_type& tt, action a, const target& t, const timestamp& mt, const prerequisite_filter& pf) { - return execute_prerequisites (&tt, a, t, mt, pf); + auto p (execute_prerequisites (&tt, a, t, mt, pf)); + return pair, const target&> (p.first, *p.second); } template - inline pair + inline pair, const T&> execute_prerequisites (const target_type& tt, action a, const target& t, const timestamp& mt, const prerequisite_filter& pf) { auto p (execute_prerequisites (tt, a, t, mt, pf)); - return make_pair (static_cast (p.first), p.second); + return pair, const T&> ( + p.first, static_cast (p.second)); } inline target_state diff --git a/build2/cc/compile.cxx b/build2/cc/compile.cxx index c5ef480..7e37c44 100644 --- a/build2/cc/compile.cxx +++ b/build2/cc/compile.cxx @@ -993,7 +993,7 @@ namespace build2 // from the depdb cache or from the compiler run. Return whether the // extraction process should be restarted. // - auto add = [&trace, &update, &pm, act, &t, lo, &dd, &bs, &t, this] + auto add = [&trace, &update, &pm, act, &t, lo, &dd, &bs, this] (path f, bool cache) -> bool { // Find or maybe insert the target. @@ -1486,22 +1486,21 @@ namespace build2 // out-of-date. Note that currently we treat all the prerequisites // as potentially affecting the result (for simplicity/performance). // - const file* s; - { - timestamp mt; + timestamp mt; - // If the depdb was overwritten or it's newer than the target, then - // do unconditional update. - // - if (md.dd_mtime == timestamp_nonexistent || - md.dd_mtime > (mt = t.load_mtime ())) - mt = timestamp_nonexistent; + // If the depdb was overwritten or it's newer than the target, then + // do unconditional update. + // + if (md.dd_mtime == timestamp_nonexistent || + md.dd_mtime > (mt = t.load_mtime ())) + mt = timestamp_nonexistent; - auto p (execute_prerequisites (x_src, act, t, mt)); + auto pr (execute_prerequisites (x_src, act, t, mt)); - if ((s = p.first) == nullptr) - return p.second; - } + if (pr.first) + return *pr.first; + + const file& s (pr.second); const scope& bs (t.base_scope ()); const scope& rs (*bs.root_scope ()); @@ -1516,7 +1515,7 @@ namespace build2 // results in easier to read diagnostics. // path relo (relative (tp)); - path rels (relative (s->path ())); + path rels (relative (s.path ())); append_options (args, t, c_poptions); append_options (args, t, x_poptions); @@ -1649,7 +1648,7 @@ namespace build2 if (verb >= 2) print_process (args); else if (verb) - text << x_name << ' ' << *s; + text << x_name << ' ' << s; try { diff --git a/build2/cli/rule.cxx b/build2/cli/rule.cxx index 884635a..2eb98c7 100644 --- a/build2/cli/rule.cxx +++ b/build2/cli/rule.cxx @@ -218,24 +218,24 @@ namespace build2 // as potentially affecting the result (think prologues/epilogues, // etc). // - const cli* s; - { - // The rule has been matched which means the members should be - // resolved and paths assigned. - // - auto p ( - execute_prerequisites ( - a, t, t.load_mtime (t.h->path ()))); - if ((s = p.first) == nullptr) - return p.second; - } + // The rule has been matched which means the members should be resolved + // and paths assigned. + // + auto pr ( + execute_prerequisites ( + a, t, t.load_mtime (t.h->path ()))); + + if (pr.first) + return *pr.first; + + const cli& s (pr.second); // Translate paths to relative (to working directory). This // results in easier to read diagnostics. // path relo (relative (t.dir)); - path rels (relative (s->path ())); + path rels (relative (s.path ())); const scope& rs (t.root_scope ()); @@ -246,7 +246,7 @@ namespace build2 // See if we need to pass --output-{prefix,suffix} // string prefix, suffix; - match_stem (t.name, s->name, &prefix, &suffix); + match_stem (t.name, s.name, &prefix, &suffix); if (!prefix.empty ()) { @@ -281,7 +281,7 @@ namespace build2 if (verb >= 2) print_process (args); else if (verb) - text << "cli " << *s; + text << "cli " << s; try { diff --git a/build2/target b/build2/target index 1a1fd8d..4316ff6 100644 --- a/build2/target +++ b/build2/target @@ -824,6 +824,7 @@ namespace build2 // struct prerequisite_member { + using scope_type = build2::scope; using target_type = build2::target; using prerequisite_type = build2::prerequisite; using target_type_type = build2::target_type; @@ -866,6 +867,12 @@ namespace build2 return target != nullptr ? target->name : prerequisite.name; } + const dir_path& + dir () const + { + return target != nullptr ? target->dir : prerequisite.dir; + } + const optional& proj () const { @@ -876,6 +883,12 @@ namespace build2 : prerequisite.proj; } + const scope_type& + scope () const + { + return target != nullptr ? target->base_scope () : prerequisite.scope; + } + const target_type& search (const target_type& t) const { diff --git a/build2/utility b/build2/utility index 2bcc52c..95766a4 100644 --- a/build2/utility +++ b/build2/utility @@ -16,8 +16,8 @@ #include -#include // combine_hash(), reverse_iterate(), casecmp(), - // lcase(), etc +#include // combine_hash(), reverse_iterate(), case*(), etc +#include #include @@ -104,6 +104,10 @@ namespace build2 // extern process_path argv0; + // Build system driver version. + // + extern butl::standard_version build_version; + // Work/home directories (must be initialized in main()) and relative path // calculation. // diff --git a/build2/utility.cxx b/build2/utility.cxx index 475b72f..46b3811 100644 --- a/build2/utility.cxx +++ b/build2/utility.cxx @@ -14,6 +14,7 @@ #include using namespace std; +using namespace butl; // // @@ -108,6 +109,7 @@ namespace build2 options ops; process_path argv0; + standard_version build_version (BUILD2_VERSION_STR); dir_path work; dir_path home; const dir_path* relative_base = &work; diff --git a/build2/version-impl b/build2/version-impl index 3df315e..b6c2a28 100644 --- a/build2/version-impl +++ b/build2/version-impl @@ -23,7 +23,7 @@ // 3.0.0-b2 02999952 // #define BUILD2_VERSION 49901 -#define BUILD2_VERSION_STR "0.5.0-a1" +#define BUILD2_VERSION_STR "0.5.0-a.1" // Generally, we expect minor versions to be source code backwards- // compatible, thought we might have a minimum version requirement. diff --git a/build2/version/init.cxx b/build2/version/init.cxx index 947f4a8..d111af5 100644 --- a/build2/version/init.cxx +++ b/build2/version/init.cxx @@ -28,6 +28,7 @@ namespace build2 static const path manifest ("manifest"); static const version_doc version_doc_; + static const version_in version_in_; void boot (scope& rs, const location& l, unique_ptr& mod) @@ -38,6 +39,7 @@ namespace build2 // Extract the version from the manifest file. // standard_version v; + dependency_constraints ds; { path f (rs.src_path () / manifest); @@ -65,7 +67,73 @@ namespace build2 { fail << "invalid standard version '" << nv.value << "': " << e; } - break; + } + else if (nv.name == "depends") + { + // According to the package manifest spec, the format of the + // 'depends' value is as follows: + // + // depends: [?][*] [; ] + // + // := [ '|' ]* + // := [] + // := | + // := ('==' | '>' | '<' | '>=' | '<=') + // := ('(' | '[') (')' | ']') + // + // Note that we don't do exhaustive validation here leaving it + // to the package manager. + // + string v (move (nv.value)); + + size_t p; + + // Get rid of the comment. + // + if ((p = v.find (';')) != string::npos) + v.resize (p); + + // Get rid of conditional/runtime markers. Note that enither of + // them is valid in the rest of the value. + // + if ((p = v.find_last_of ("?*")) != string::npos) + v.erase (0, p + 1); + + // Parse as |-separated "words". + // + for (size_t b (0), e (0); next_word (v, b, e, '|'); ) + { + string d (v, b, e - b); + trim (d); + + p = d.find_first_of (" \t=<>[("); + string n (d, 0, p); + string c (p != string::npos ? string (d, p) : string ()); + + trim (n); + trim (c); + + // If this is a dependency on the build system itself, check + // it (so there is no need for explicit using build@X.Y.Z). + // + if (n == "build2" && !c.empty ()) + try + { + standard_version_constraint vc (c); + + if (!vc.satisfies (build_version)) + fail (l) << "incompatible build2 version" << + info << "running " << build_version.string () << + info << "required " << vc.string (); + } + catch (const invalid_argument& e) + { + fail (l) << "invalid version constraint for dependency " + << b << ": " << e; + } + + ds.emplace (move (n), move (c)); + } } } } @@ -91,7 +159,6 @@ namespace build2 // snapshot sn and id (e.g., commit date and id from git). If there is // uncommitted stuff, then leave it as .z. // - bool patched (false); if (v.snapshot () && v.snapshot_sn == standard_version::latest_sn) { snapshot ss (extract_snapshot (rs)); @@ -100,7 +167,6 @@ namespace build2 { v.snapshot_sn = ss.sn; v.snapshot_id = move (ss.id); - patched = true; } } @@ -155,7 +221,7 @@ namespace build2 // Create the module. // - mod.reset (new module (move (v), patched)); + mod.reset (new module (move (v), move (ds))); } static void @@ -211,6 +277,10 @@ namespace build2 r.insert (perform_update_id, "version.doc", version_doc_); r.insert (perform_clean_id, "version.doc", version_doc_); r.insert (configure_update_id, "version.doc", version_doc_); + + r.insert (perform_update_id, "version.in", version_in_); + r.insert (perform_clean_id, "version.in", version_in_); + r.insert (configure_update_id, "version.in", version_in_); } return true; diff --git a/build2/version/module b/build2/version/module index d5c1f01..6f913b3 100644 --- a/build2/version/module +++ b/build2/version/module @@ -5,6 +5,8 @@ #ifndef BUILD2_VERSION_MODULE #define BUILD2_VERSION_MODULE +#include + #include #include @@ -16,15 +18,19 @@ namespace build2 { namespace version { + // The 'depends' values from manifest. + // + using dependency_constraints = std::map; + struct module: module_base { static const string name; butl::standard_version version; - bool version_patched; // True if snapshot was patched in. + dependency_constraints dependencies; - module (butl::standard_version v, bool vp) - : version (move (v)), version_patched (vp) {} + module (butl::standard_version v, dependency_constraints d) + : version (move (v)), dependencies (move (d)) {} }; } } diff --git a/build2/version/rule b/build2/version/rule index 75a8c12..186df59 100644 --- a/build2/version/rule +++ b/build2/version/rule @@ -30,6 +30,23 @@ namespace build2 static target_state perform_update (action, const target&); }; + + // Preprocess an .in file. + // + class version_in: public rule + { + public: + version_in () {} + + virtual match_result + match (action, target&, const string&) const override; + + virtual recipe + apply (action, target&) const override; + + static target_state + perform_update (action, const target&); + }; } } diff --git a/build2/version/rule.cxx b/build2/version/rule.cxx index b6f09de..6909b92 100644 --- a/build2/version/rule.cxx +++ b/build2/version/rule.cxx @@ -4,9 +4,11 @@ #include +#include #include #include #include +#include #include #include #include @@ -20,10 +22,35 @@ namespace build2 { namespace version { + // Return true if this prerequisite looks like a project's manifest file. + // To be sure we would need to search it into target but that we can't + // do in match(). + // + static inline bool + manifest_prerequisite (const scope& rs, const prerequisite_member& p) + { + if (!p.is_a () || p.name () != "manifest") + return false; + + const scope& s (p.scope ()); + + if (s.root_scope () == nullptr) // Out of project prerequisite. + return false; + + dir_path d (p.dir ()); + if (d.relative ()) + d = s.src_path () / d; + d.normalize (); + + return d == rs.src_path (); + } + + // version_doc + // match_result version_doc:: match (action a, target& xt, const string&) const { - tracer trace ("version::version_file::match"); + tracer trace ("version::version_doc::match"); doc& t (static_cast (xt)); @@ -41,20 +68,8 @@ namespace build2 for (prerequisite_member p: group_prerequisite_members (a, t)) { - if (!p.is_a ()) - continue; - - const target& pt (p.search (t)); - - if (pt.name != "manifest") - continue; - - const scope* prs (pt.base_scope ().root_scope ()); - - if (prs == nullptr || prs != &rs || pt.dir != rs.src_path ()) - continue; - - return true; + if (manifest_prerequisite (rs, p)) + return true; } l4 ([&]{trace << "no manifest prerequisite for target " << t;}); @@ -66,7 +81,7 @@ namespace build2 { doc& t (static_cast (xt)); - // Derive file names for the members. + // Derive the file name. // t.derive_path (); @@ -90,7 +105,7 @@ namespace build2 perform_update (action a, const target& xt) { const doc& t (xt.as ()); - const path& f (t.path ()); + const path& tp (t.path ()); const scope& rs (t.root_scope ()); const module& m (*rs.modules.lookup (module::name)); @@ -102,46 +117,447 @@ namespace build2 // module (we checked above that manifest and version files are in the // same project). // - // That is, unless we patched the snapshot information in, in which case - // we have to compare the contents. + // We also have to compare the contents (essentially using the file as + // its own depdb) in case of a snapshot since we can go back and forth + // between committed and uncommitted state that doesn't depend on any of + // our prerequisites. // { - auto p (execute_prerequisites (a, t, t.load_mtime ())); + optional ts ( + execute_prerequisites (a, t, t.load_mtime ())); - if (!p.first) + if (ts) { - if (!m.version_patched || !exists (f)) - return p.second; + if (!m.version.snapshot ()) // Everything came from the manifest. + return *ts; try { - ifdstream ifs (f, fdopen_mode::in, ifdstream::badbit); + ifdstream ifs (tp, fdopen_mode::in, ifdstream::badbit); string s; getline (ifs, s); if (s == m.version.string_project ()) - return p.second; + return *ts; } catch (const io_error& e) { - fail << "unable to read " << f << ": " << e; + fail << "unable to read " << tp << ": " << e; } } } if (verb >= 2) - text << "cat >" << f; + text << "cat >" << tp; try { - ofdstream ofs (f); + ofdstream ofs (tp); + auto_rmfile arm (tp); ofs << m.version.string_project () << endl; ofs.close (); + arm.cancel (); + } + catch (const io_error& e) + { + fail << "unable to write " << tp << ": " << e; + } + + t.mtime (system_clock::now ()); + return target_state::changed; + } + + // version_in + // + match_result version_in:: + match (action a, target& xt, const string&) const + { + tracer trace ("version::version_in::match"); + + file& t (static_cast (xt)); + const scope& rs (t.root_scope ()); + + bool fm (false); // Found manifest. + bool fi (false); // Found in. + for (prerequisite_member p: group_prerequisite_members (a, t)) + { + fm = fm || manifest_prerequisite (rs, p); + fi = fi || p.is_a (); + } + + if (!fm) + l4 ([&]{trace << "no manifest prerequisite for target " << t;}); + + if (!fi) + l4 ([&]{trace << "no in file prerequisite for target " << t;}); + + return fm && fi; + } + + recipe version_in:: + apply (action a, target& xt) const + { + file& t (static_cast (xt)); + + // Derive the file name. + // + t.derive_path (); + + // Inject dependency on the output directory. + // + inject_fsdir (a, t); + + // Match prerequisite members. + // + match_prerequisite_members (a, t); + + switch (a) + { + case perform_update_id: return &perform_update; + case perform_clean_id: return &perform_clean_depdb; // Standard clean. + default: return noop_recipe; // Configure update. + } + } + + target_state version_in:: + perform_update (action a, const target& xt) + { + tracer trace ("version::version_in::perform_update"); + + const file& t (xt.as ()); + const path& tp (t.path ()); + + const scope& rs (t.root_scope ()); + const module& m (*rs.modules.lookup (module::name)); + + // Determine if anything needs to be updated. + // + timestamp mt (t.load_mtime ()); + auto pr (execute_prerequisites (a, t, mt)); + + bool update (!pr.first); + target_state ts (update ? target_state::changed : *pr.first); + + const in& i (pr.second); + const path& ip (i.path ()); + + // We use depdb to track both the .in file and the potentially patched + // snapshot. + // + { + depdb dd (tp + ".d"); + + // First should come the rule name/version. + // + if (dd.expect ("version.in 1") != nullptr) + l4 ([&]{trace << "rule mismatch forcing update of " << t;}); + + // Then the .in file. + // + if (dd.expect (i.path ()) != nullptr) + l4 ([&]{trace << "in file mismatch forcing update of " << t;}); + + // Finally the snapshot info. + // + if (dd.expect (m.version.string_snapshot ()) != nullptr) + l4 ([&]{trace << "snapshot mismatch forcing update of " << t;}); + + // Update if depdb mismatch. + // + if (dd.writing () || dd.mtime () > mt) + update = true; + + dd.close (); + } + + // If nothing changed, then we are done. + // + if (!update) + return ts; + + const string& proj (cast (rs.vars[var_project])); + + // Perform substitutions for the project itself (normally the version.* + // variables but we allow anything set on the root scope). + // + auto subst_self = [&rs, &m] (const location& l, const string& s) + { + if (lookup x = rs.vars[s]) + { + // Call the string() function to convert the value. + // + value v (*x); + + return convert ( + functions.call ( + "string", vector_view (&v, 1), l)); + } + else + fail (l) << "undefined project variable '" << s << "'" << endf; + }; + + // Perform substitutions for a dependency. Here we recognize the + // following substitutions: + // + // $libfoo.version$ - textual version constraint. + // $libfoo.condition(VER[,SNAP])$ - numeric satisfaction condition. + // $libfoo.check(VER[,SNAP])$ - numeric satisfaction check (#if ...). + // + // Where VER is the version number macro and SNAP is the optional + // snapshot number macro (only needed if you plan to include snapshot + // informaton in your constraints). + // + auto subst_dep = [&m] (const location& l, + const string& n, + const string& s) + { + // For now we re-parse the constraint every time. Firstly because + // not all of them are necessarily in the standard form and secondly + // because of the MT-safety. + // + standard_version_constraint c; + + try + { + auto i (m.dependencies.find (n)); + + if (i == m.dependencies.end ()) + fail (l) << "unknown dependency '" << n << "'"; + + if (i->second.empty ()) + fail (l) << "no version constraint for dependency " << n; + + c = standard_version_constraint (i->second); + } + catch (const invalid_argument& e) + { + fail (l) << "invalid version constraint for dependency " << n + << ": " << e; + } + + // Now substitute. + // + size_t i; + if (s == "version") + { + return c.string (); // Use normalized representation. + } + if (s.compare (0, (i = 6), "check(") == 0 || + s.compare (0, (i = 10), "condition(") == 0) + { + size_t j (s.find_first_of (",)", i)); + + if (j == string::npos || (s[j] == ',' && s.back () != ')')) + fail (l) << "missing closing ')'"; + + string vm (s, i, j - i); // VER macro. + string sm (s[j] == ',' // SNAP macro. + ? string (s, j + 1, s.size () - j - 2) + : string ()); + + trim (vm); + trim (sm); + + auto cond = [&l, &c, &vm, &sm] () -> string + { + auto& miv (c.min_version); + auto& mav (c.max_version); + + bool mio (c.min_open); + bool mao (c.max_open); + + if (sm.empty () && + ((miv && miv->snapshot ()) || + (mav && mav->snapshot ()))) + fail (l) << "snapshot macro required for " << c.string (); + + auto cmp = [] (const string& m, const char* o, uint64_t v) + { + return m + o + to_string (v) + "ULL"; + }; + + // Note that version orders everything among pre-releases (that + // E being 0/1). So the snapshot comparison is only necessary + // "inside" the same pre-release. + // + auto max_cmp = [&vm, &sm, mao, &mav, &cmp] (bool p = false) + { + string r; + + if (mav->snapshot ()) + { + r += (p ? "(" : ""); + + r += cmp (vm, " < ", mav->version) + " || ("; + r += cmp (vm, " == ", mav->version) + " && "; + r += cmp (sm, (mao ? " < " : " <= "), mav->snapshot_sn) + ")"; + + r += (p ? ")" : ""); + } + else + r = cmp (vm, (mao ? " < " : " <= "), mav->version); + + return r; + }; + + auto min_cmp = [&vm, &sm, mio, &miv, &cmp] (bool p = false) + { + string r; + + if (miv->snapshot ()) + { + r += (p ? "(" : ""); + + r += cmp (vm, " > ", miv->version) + " || ("; + r += cmp (vm, " == ", miv->version) + " && "; + r += cmp (sm, (mio ? " > " : " >= "), miv->snapshot_sn) + ")"; + + r += (p ? ")" : ""); + } + else + r = cmp (vm, (mio ? " > " : " >= "), miv->version); + + return r; + }; + + // < / <= + // + if (!miv) + return max_cmp (); + + // > / >= + // + if (!mav) + return min_cmp (); + + // == + // + if (*miv == *mav) + { + string r (cmp (vm, " == ", miv->version)); + + if (miv->snapshot ()) + r += " && " + cmp (sm, " == ", miv->snapshot_sn); + + return r; + } + + // range + // + return min_cmp (true) + " && " + max_cmp (true); + }; + + if (s[1] == 'o') // condition + return cond (); + + string r; + + r += "#if !(" + cond () + ")\n"; + r += "# error incompatible " + n + " version, "; + r += n + ' ' + c.string () + " is required\n"; + r += "#endif"; + + return r; + } + else + fail (l) << "unknown dependency substitution '" << s << "'" << endf; + }; + + if (verb >= 2) + text << "ver -o " << tp << ' ' << ip; + else if (verb) + text << "ver " << tp; + + // Read and process the file, one line at a time. + // + const char* what; + const path* whom; + try + { + what = "open"; whom = &ip; + ifdstream ifs (ip, fdopen_mode::in, ifdstream::badbit); + + what = "open"; whom = &tp; + ofdstream ofs (tp); + auto_rmfile arm (tp); + + string s; // Reuse the buffer. + for (size_t ln (1);; ++ln) + { + what = "read"; whom = &ip; + if (!getline (ifs, s)) + break; // Could not read anything, not even newline. + + const location l (&ip, ln); // Not tracking column for now. + + // Scan the line looking for substiutions in the $.$ + // form. Treat $$ as an escape sequence. + // + for (size_t b (0), n, d; b != (n = s.size ()); b += d) + { + d = 1; + + if (s[b] != '$') + continue; + + // Find the other end. + // + size_t e (b + 1); + for (; e != (n = s.size ()); ++e) + { + if (s[e] == '$') + { + if (e + 1 != n && s[e + 1] == '$') // Escape. + s.erase (e, 1); // Keep one, erase the other. + else + break; + } + } + + if (e == n) + fail (l) << "unterminated '$'"; + + if (e - b == 1) // Escape. + { + s.erase (b, 1); // Keep one, erase the other. + continue; + } + + // We have a substition with b pointing to the opening $ and e -- + // to the closing. Split it into the package name and the trailer. + // + size_t p (s.find ('.', b + 1)); + + if (p == string::npos || p > e) + fail (l) << "invalid substitution: missing package name"; + + string sn (s, b + 1, p - b - 1); + string st (s, p + 1, e - p - 1); + string sr (sn == proj + ? subst_self (l, st) + : subst_dep (l, sn, st)); + + // Patch the result in and adjust the delta. + // + s.replace (b, e - b + 1, sr); + d = sr.size (); + } + + what = "write"; whom = &tp; + ofs << s << endl; + } + + what = "close"; whom = &tp; + ofs.close (); + arm.cancel (); + + what = "close"; whom = &ip; + ifs.close (); } catch (const io_error& e) { - fail << "unable to write " << f << ": " << e; + fail << "unable to " << what << ' ' << whom << ": " << e; } t.mtime (system_clock::now ()); -- cgit v1.1