From fe6f3ec0868185511f5acefb2729eb879798f052 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Tue, 17 Jul 2018 13:18:17 +0200 Subject: Reimplement version::in_rule in terms of in::rule Significantly, the version::in_rule rule now track changes to the substitution values. --- build2/in/init.cxx | 2 +- build2/in/rule.cxx | 11 +- build2/in/rule.hxx | 19 +- build2/version/init.cxx | 13 +- build2/version/module.hxx | 11 +- build2/version/rule.cxx | 552 ++++++++++++---------------------------------- build2/version/rule.hxx | 15 +- 7 files changed, 182 insertions(+), 441 deletions(-) diff --git a/build2/in/init.cxx b/build2/in/init.cxx index 337722a..a87f8b3 100644 --- a/build2/in/init.cxx +++ b/build2/in/init.cxx @@ -18,7 +18,7 @@ namespace build2 { namespace in { - static const rule rule_; + static const rule rule_ ("in", "in"); bool base_init (scope& rs, diff --git a/build2/in/rule.cxx b/build2/in/rule.cxx index de43e4d..6be2e71 100644 --- a/build2/in/rule.cxx +++ b/build2/in/rule.cxx @@ -55,7 +55,7 @@ namespace build2 // Derive the file name. // - // If this is an executable with an uspecified extension, then default + // If this is an executable with an unspecified extension, then default // to no extension (i.e., a shell script). // t.derive_path (t.is_a () ? "" : nullptr); @@ -187,7 +187,7 @@ namespace build2 // First should come the rule name/version. // - if (dd.expect ("in 1") != nullptr) + if (dd.expect (rule_id_ + " 1") != nullptr) l4 ([&]{trace << "rule mismatch forcing update of " << t;}); // Then the substitution symbol. @@ -253,6 +253,9 @@ namespace build2 // // // + // Note that can contain spaces (see the constraint check + // expressions in the version module). + // char* e (nullptr); uint64_t ln (strtoull (s->c_str (), &e, 10)); @@ -296,9 +299,9 @@ namespace build2 } if (verb >= 2) - text << "in " << ip << " >" << tp; + text << program_ << ' ' << ip << " >" << tp; else if (verb) - text << "in " << ip; + text << program_ << ' ' << ip; // Read and process the file, one line at a time. // diff --git a/build2/in/rule.hxx b/build2/in/rule.hxx index a68e80c..17fbaed 100644 --- a/build2/in/rule.hxx +++ b/build2/in/rule.hxx @@ -16,11 +16,24 @@ namespace build2 { // Preprocess an .in file. // + // Note that a derived rule can use the target data pad to cache data + // (e.g., in match()) to be used in substitute/lookup() calls. + // class rule: public build2::rule { public: - rule (char symbol = '$', bool strict = true) - : symbol_ (symbol), strict_ (strict) {} + // The rule id is used to form the rule name/version entry in depdb. The + // program argument is the pseudo-program name to use in the command + // line diagnostics. + // + rule (string rule_id, + string program, + char symbol = '$', + bool strict = true) + : rule_id_ (move (rule_id)), + program_ (move (program)), + symbol_ (symbol), + strict_ (strict) {} virtual bool match (action, target&, const string&) const override; @@ -46,6 +59,8 @@ namespace build2 perform_update (action, const target&) const; protected: + const string rule_id_; + const string program_; char symbol_; bool strict_; }; diff --git a/build2/version/init.cxx b/build2/version/init.cxx index d2df772..397d7da 100644 --- a/build2/version/init.cxx +++ b/build2/version/init.cxx @@ -232,7 +232,11 @@ namespace build2 // Create the module. // - mod.reset (new module (move (v), committed, rewritten, move (ds))); + mod.reset (new module (cast (rs.vars[var_project]), + move (v), + committed, + rewritten, + move (ds))); return true; // Init first (dist.package, etc). } @@ -295,13 +299,6 @@ namespace build2 } } - // Enter variables. - // - { - m.in_symbol = var_pool.find ("in.symbol"); // in.base - m.in_substitution = var_pool.find ("in.substitution"); // in.base - } - // Register rules. // { diff --git a/build2/version/module.hxx b/build2/version/module.hxx index b3bf813..56536c3 100644 --- a/build2/version/module.hxx +++ b/build2/version/module.hxx @@ -24,22 +24,23 @@ namespace build2 { static const string name; + const string& project; // The project variable value. + butl::standard_version version; bool committed; // Whether this is a committed snapshot. bool rewritten; // Whether this is a rewritten .z snapshot. dependency_constraints dependencies; - const variable* in_symbol = nullptr; // in.symbol - const variable* in_substitution = nullptr; // in.substitution - bool dist_uncommitted = false; - module (butl::standard_version v, + module (const string& p, + butl::standard_version v, bool c, bool r, dependency_constraints d) - : version (move (v)), + : project (p), + version (move (v)), committed (c), rewritten (r), dependencies (move (d)) {} diff --git a/build2/version/rule.cxx b/build2/version/rule.cxx index 0250a15..a0ff1e2 100644 --- a/build2/version/rule.cxx +++ b/build2/version/rule.cxx @@ -77,160 +77,41 @@ namespace build2 if (!fi) l4 ([&]{trace << "no in file prerequisite for target " << t;}); - return fm && fi; - } - - recipe in_rule:: - 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); + bool r (fm && fi); - // Match prerequisite members. + // If we match, lookup and cache the module for the update operation. // - match_prerequisite_members (a, t); + if (r && a == perform_update_id) + t.data (rs.modules.lookup (module::name)); - 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. - } + return r; } - target_state in_rule:: - perform_update (action a, const target& xt) + string in_rule:: + lookup (const location& l, const target& t, const string& n) const { - tracer trace ("version::in_rule::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)); - - // The substitution symbol can be overridden with the in.symbol - // variable. + // Note that this code will be executed during up-to-date check for each + // substitution so let's try not to do anything overly sub-optimal here. // - char sym ('$'); - if (const string* s = cast_null (t[m.in_symbol])) - { - if (s->size () == 1) - sym = s->front (); - else - fail << "invalid substitution symbol '" << *s << "'"; - } + const module& m (*t.data ()); - // The substitution mode can be overridden with the in.substitution - // variable. + // Split it into the package name and the variable/condition name. // - bool strict (true); - if (const string* s = cast_null (t[m.in_substitution])) - { - if (*s == "lax") - strict = false; - else if (*s != "strict") - fail << "invalid substitution mode '" << *s << "'"; - } - - // Determine if anything needs to be updated. + // We used to bail if there is no package component but now we treat it + // the same as project. This can be useful when trying to reuse existing + // .in files (e.g., from autoconf, etc). // - 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); + size_t p (n.find ('.')); - const in& i (pr.second); - const path& ip (i.path ()); - - // We use depdb to track both the .in file and the potentially patched - // snapshot. - // + if (p == string::npos || n.compare (0, p, m.project) == 0) { - depdb dd (tp + ".d"); - - // First should come the rule name/version. - // - if (dd.expect ("version.in 3") != nullptr) - l4 ([&]{trace << "rule mismatch forcing update of " << t;}); - - // Then the substitution symbol. - // - if (dd.expect (string (1, sym)) != nullptr) - l4 ([&]{trace << "substitution symbol mismatch forcing update of" - << t;}); - - // Then the substitution mode. - // - if (dd.expect (strict ? "strict" : "lax") != nullptr) - l4 ([&]{trace << "substitution mode mismatch forcing update of" - << t;}); - - // Then the .in file. + // Standard lookup. // - 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; - - //@@ TODO: what if one of the substituted non-version values is - // changes. See how we handle this in the .in module. In fact, - // it feels like this should be an extended version of in. - - dd.close (); + return rule::lookup (l, t, p == string::npos ? n : string (n, p + 1)); } - // 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 target and up). - // - auto subst_self = [&t] (const location& l, const string& s) -> string - { - if (lookup x = t[s]) - { - value v (*x); - - // For typed values call the string() function to convert it. - // - try - { - return convert ( - v.type == nullptr - ? move (v) - : functions.call ( - &t.base_scope (), "string", vector_view (&v, 1), l)); - } - catch (const invalid_argument& e) - { - fail (l) << e << - info << "while substituting '" << s << "'" << endf; - } - } - else - fail (l) << "undefined project variable '" << s << "'" << endf; - }; + string pn (n, 0, p); + string vn (n, p + 1); // Perform substitutions for a dependency. Here we recognize the // following substitutions: @@ -243,319 +124,166 @@ namespace build2 // 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)); + // Note also that the last two (condition and check) can only be used in + // the strict substitution mode since in::rule::substitute() will skip + // them in the lax mode. - if (i == m.dependencies.end ()) - fail (l) << "unknown dependency '" << n << "'"; - - if (i->second.empty ()) - fail (l) << "no version constraint for dependency " << n; + // 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; - c = standard_version_constraint (i->second); - } - catch (const invalid_argument& e) - { - fail (l) << "invalid version constraint for dependency " << n - << ": " << e; - } + try + { + auto i (m.dependencies.find (pn)); - // 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 (i == m.dependencies.end ()) + fail (l) << "unknown dependency '" << pn << "'"; - if (j == string::npos || (s[j] == ',' && s.back () != ')')) - fail (l) << "missing closing ')'"; + if (i->second.empty ()) + fail (l) << "no version constraint for dependency " << pn; - string vm (s, i, j - i); // VER macro. - string sm (s[j] == ',' // SNAP macro. - ? string (s, j + 1, s.size () - j - 2) - : string ()); + c = standard_version_constraint (i->second); + } + catch (const invalid_argument& e) + { + fail (l) << "invalid version constraint for dependency " << pn + << ": " << e; + } - trim (vm); - trim (sm); + // Now substitute. + // + size_t i; + if (vn == "version") + { + return c.string (); // Use normalized representation. + } + if (vn.compare (0, (i = 6), "check(") == 0 || + vn.compare (0, (i = 10), "condition(") == 0) + { + size_t j (vn.find_first_of (",)", i)); - auto cond = [&l, &c, &vm, &sm] () -> string - { - auto& miv (c.min_version); - auto& mav (c.max_version); + if (j == string::npos || (vn[j] == ',' && vn.back () != ')')) + fail (l) << "missing closing ')'"; - bool mio (c.min_open); - bool mao (c.max_open); + string vm (vn, i, j - i); // VER macro. + string sm (vn[j] == ',' // SNAP macro. + ? string (vn, j + 1, vn.size () - j - 2) + : string ()); - if (sm.empty () && - ((miv && miv->snapshot ()) || - (mav && mav->snapshot ()))) - fail (l) << "snapshot macro required for " << c.string (); + trim (vm); + trim (sm); - 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; + auto cond = [&l, &c, &vm, &sm] () -> string + { + auto& miv (c.min_version); + auto& mav (c.max_version); - if (mav->snapshot ()) - { - r += (p ? "(" : ""); + bool mio (c.min_open); + bool mao (c.max_open); - r += cmp (vm, " < ", mav->version) + " || ("; - r += cmp (vm, " == ", mav->version) + " && "; - r += cmp (sm, (mao ? " < " : " <= "), mav->snapshot_sn) + ")"; + if (sm.empty () && + ((miv && miv->snapshot ()) || + (mav && mav->snapshot ()))) + fail (l) << "snapshot macro required for " << c.string (); - r += (p ? ")" : ""); - } - else - r = cmp (vm, (mao ? " < " : " <= "), mav->version); + auto cmp = [] (const string& m, const char* o, uint64_t v) + { + return m + o + to_string (v) + "ULL"; + }; - return r; - }; + // 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; - auto min_cmp = [&vm, &sm, mio, &miv, &cmp] (bool p = false) + if (mav->snapshot ()) { - string r; - - if (miv->snapshot ()) - { - r += (p ? "(" : ""); + r += (p ? "(" : ""); - r += cmp (vm, " > ", miv->version) + " || ("; - r += cmp (vm, " == ", miv->version) + " && "; - r += cmp (sm, (mio ? " > " : " >= "), miv->snapshot_sn) + ")"; + r += cmp (vm, " < ", mav->version) + " || ("; + r += cmp (vm, " == ", mav->version) + " && "; + r += cmp (sm, (mao ? " < " : " <= "), mav->snapshot_sn) + ")"; - r += (p ? ")" : ""); - } - else - r = cmp (vm, (mio ? " > " : " >= "), miv->version); - - return r; - }; + r += (p ? ")" : ""); + } + else + r = cmp (vm, (mao ? " < " : " <= "), mav->version); - // < / <= - // - if (!miv) - return max_cmp (); + return r; + }; - // > / >= - // - if (!mav) - return min_cmp (); + auto min_cmp = [&vm, &sm, mio, &miv, &cmp] (bool p = false) + { + string r; - // == - // - if (*miv == *mav) + if (miv->snapshot ()) { - string r (cmp (vm, " == ", miv->version)); + r += (p ? "(" : ""); - if (miv->snapshot ()) - r += " && " + cmp (sm, " == ", miv->snapshot_sn); + r += cmp (vm, " > ", miv->version) + " || ("; + r += cmp (vm, " == ", miv->version) + " && "; + r += cmp (sm, (mio ? " > " : " >= "), miv->snapshot_sn) + ")"; - return r; + r += (p ? ")" : ""); } + else + r = cmp (vm, (mio ? " > " : " >= "), miv->version); - // range - // - return min_cmp (true) + " && " + max_cmp (true); + return r; }; - if (s[1] == 'o') // condition - return cond (); - - string r; - - // This is tricky: if the version header hasn't been generated yet, - // then the check will fail. Maybe a better solution is to disable - // diagnostics and ignore (some) errors during dependency - // extraction. + // < / <= // - r += "#ifdef " + vm + "\n"; - r += "# if !(" + cond () + ")\n"; - r += "# error incompatible " + n + " version, "; - r += n + ' ' + c.string () + " is required\n"; - r += "# endif\n"; - r += "#endif"; - - return r; - } - else - fail (l) << "unknown dependency substitution '" << s << "'" << endf; - }; - - if (verb >= 2) - text << "ver " << ip << " >" << tp; - else if (verb) - text << "ver " << ip; - - // 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); + if (!miv) + return max_cmp (); - 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. + // > / >= + // + if (!mav) + return min_cmp (); - // Scan the line looking for substiutions in the $.$ - // form. In the strict mode treat $$ as an escape sequence. + // == // - for (size_t b (0), n, d; b != (n = s.size ()); b += d) + if (*miv == *mav) { - d = 1; - - if (s[b] != sym) - continue; - - // Note that in the lax mode these should still be substitutions: - // - // @project@@ - // @@project@ + string r (cmp (vm, " == ", miv->version)); - // Find the other end. - // - size_t e (b + 1); - for (; e != (n = s.size ()); ++e) - { - if (s[e] == sym) - { - if (strict && e + 1 != n && s[e + 1] == sym) // Escape. - s.erase (e, 1); // Keep one, erase the other. - else - break; - } - } - - if (e == n) - { - if (strict) - fail (l) << "unterminated '" << sym << "'" << endf; + if (miv->snapshot ()) + r += " && " + cmp (sm, " == ", miv->snapshot_sn); - break; - } - - if (e - b == 1) // Escape (or just double symbol in the lax mode). - { - if (strict) - s.erase (b, 1); // Keep one, erase the other. - - continue; - } - - // We have a (potential in the lax mode) substition with b - // pointing to the opening symbol and e -- to the closing. - // - if (!strict) - { - // Scan the fragment to make sure it is a variable name (that - // is, it can be expanded as just $; see lexer's variable - // mode for details). - // - size_t i; - for (i = b + 1; i != e; ) - { - bool f (i == b + 1); // First. - char c (s[i++]); - bool l (i == e); // Last. - - if (c == '_' || (f ? alpha (c) : alnum (c))) - continue; - - if (c == '.' && !l) - continue; - - i = string::npos; - break; - } - - if (i == string::npos) - { - d = e - b + 1; // Ignore this substitution. - continue; - } - } - - // Split it into the package name and the trailer. - // - // We used to bail if there is no package component but now we - // treat it the same as project. This can be useful when trying to - // reuse existing .in files (e.g., from autoconf, etc). - // - string sn, st; - size_t p (s.find ('.', b + 1)); - - if (p != string::npos && p < e) - { - sn.assign (s, b + 1, p - b - 1); - st.assign (s, p + 1, e - p - 1); - } - else - st.assign (s, b + 1, e - b - 1); - - string sr (sn.empty () || 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 (); + return r; } - what = "write"; whom = &tp; - ofs << s << endl; - } + // range + // + return min_cmp (true) + " && " + max_cmp (true); + }; - what = "close"; whom = &tp; - ofs.close (); - arm.cancel (); + if (vn[1] == 'o') // condition + return cond (); - what = "close"; whom = &ip; - ifs.close (); - } - catch (const io_error& e) - { - fail << "unable to " << what << ' ' << *whom << ": " << e; - } + string r; - t.mtime (system_clock::now ()); - return target_state::changed; + // This is tricky: if the version header hasn't been generated yet, + // then the check will fail. Maybe a better solution is to disable + // diagnostics and ignore (some) errors during dependency extraction. + // + r += "#ifdef " + vm + "\n"; + r += "# if !(" + cond () + ")\n"; + r += "# error incompatible " + pn + " version, "; + r += pn + ' ' + c.string () + " is required\n"; + r += "# endif\n"; + r += "#endif"; + + return r; + } + else + fail (l) << "unknown dependency substitution '" << vn << "'" << endf; } // manifest_install_rule diff --git a/build2/version/rule.hxx b/build2/version/rule.hxx index b01383f..afed11a 100644 --- a/build2/version/rule.hxx +++ b/build2/version/rule.hxx @@ -8,28 +8,25 @@ #include #include -#include +#include #include namespace build2 { namespace version { - // Preprocess an .in file. + // Preprocess an .in file that depends on manifest. // - class in_rule: public rule + class in_rule: public in::rule { public: - in_rule () {} + in_rule (): rule ("version.in 1", "ver") {} virtual bool match (action, target&, const string&) const override; - virtual recipe - apply (action, target&) const override; - - static target_state - perform_update (action, const target&); + virtual string + lookup (const location&, const target&, const string&) const override; }; // Pre-process manifest before installation to patch in the version. -- cgit v1.1