From 309cccfffc15657dd8654aa6a14e444bb47417ae Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Mon, 21 Feb 2022 22:54:05 +0300 Subject: Increment version iteration for external packages on buildfiles change --- bpkg/manifest-utility.cxx | 60 ++++++++ bpkg/manifest-utility.hxx | 9 ++ bpkg/package.cxx | 31 ++--- bpkg/package.hxx | 21 ++- bpkg/package.ixx | 19 +++ bpkg/package.xml | 6 + bpkg/pkg-build.cxx | 2 +- bpkg/pkg-checkout.cxx | 13 ++ bpkg/pkg-configure.cxx | 1 + bpkg/pkg-fetch.cxx | 1 + bpkg/pkg-unpack.cxx | 138 ++++++++++++++++--- bpkg/pkg-unpack.hxx | 6 +- tests/pkg-build.testscript | 333 +++++++++++++++++++++++++++++++++------------ 13 files changed, 511 insertions(+), 129 deletions(-) diff --git a/bpkg/manifest-utility.cxx b/bpkg/manifest-utility.cxx index fc54eba..f690a40 100644 --- a/bpkg/manifest-utility.cxx +++ b/bpkg/manifest-utility.cxx @@ -357,4 +357,64 @@ namespace bpkg fail << "unable to read from " << f << ": " << e << endf; } } + + string + package_buildfiles_checksum (const optional& bb, + const optional& rb, + const dir_path& d) + { + if (bb && rb) + { + sha256 cs (*bb); + cs.append (*rb); + return cs.string (); + } + + auto checksum = [&bb, &rb] (const path& b, const path& r) + { + sha256 cs; + + auto append_file = [&cs] (const path& f) + { + try + { + // Open the buildfile in the text mode and hash the NULL character + // at the end to calculate the checksum over files consistently with + // calculating it over the *-build manifest values. + // + ifdstream ifs (f); + cs.append (ifs); + cs.append ('\0'); + } + catch (const io_error& e) + { + fail << "unable to read from " << f << ": " << e; + } + }; + + if (bb) + cs.append (*bb); + else + append_file (b); + + if (rb) + cs.append (*rb); + else if (exists (r)) + append_file (r); + + return string (cs.string ()); + }; + + // Check the alternative bootstrap file first since it is more + // specific. + // + path bf; + if (exists (bf = d / alt_bootstrap_file)) + return checksum (bf, d / alt_root_file); + else if (exists (bf = d / std_bootstrap_file)) + return checksum (bf, d / std_root_file); + else + fail << "unable to find bootstrap.build file in package directory " + << d << endf; + } } diff --git a/bpkg/manifest-utility.hxx b/bpkg/manifest-utility.hxx index f293ef5..8701b65 100644 --- a/bpkg/manifest-utility.hxx +++ b/bpkg/manifest-utility.hxx @@ -153,6 +153,15 @@ namespace bpkg package_checksum (const common_options&, const dir_path& src_dir, const package_info*); + + // Caclulate the checksum of the buildfiles using the *-build manifest + // values, unless unspecified in which case use the files in the package + // source directory. + // + string + package_buildfiles_checksum (const optional& bootstrap_build, + const optional& root_build, + const dir_path& src_dir); } #endif // BPKG_MANIFEST_UTILITY_HXX diff --git a/bpkg/package.cxx b/bpkg/package.cxx index c8ad9d6..da560f2 100644 --- a/bpkg/package.cxx +++ b/bpkg/package.cxx @@ -654,7 +654,21 @@ namespace bpkg bool changed (mc != *p->manifest_checksum); - // If the manifest didn't changed but the selected package points to an + // If the manifest hasn't changed and the package has buildfile clauses in + // the dependencies, then check if the buildfiles haven't changed either. + // + if (!changed && p->buildfiles_checksum) + { + // Always calculate the checksum over the buildfiles since the package + // is external. + // + changed = package_buildfiles_checksum ( + nullopt /* bootstrap_build */, + nullopt /* root_build */, + d) != *p->buildfiles_checksum; + } + + // If the manifest hasn't changed but the selected package points to an // external source directory, then we also check if the directory have // moved. // @@ -840,19 +854,4 @@ namespace bpkg return false; } - - bool - evaluate_enabled (const dependency_alternative& da, - const string& /* bootstrap_build */, - const optional& /* root_build */, - const package_name& pkg) - { - // @@ DEP TMP - // - if (da.enable) - fail << "conditional dependency for package " << pkg << - info << "conditional dependencies are not yet supported"; - - return true; - } } diff --git a/bpkg/package.hxx b/bpkg/package.hxx index ad2b7a8..26eb34b 100644 --- a/bpkg/package.hxx +++ b/bpkg/package.hxx @@ -27,7 +27,7 @@ // #define DB_SCHEMA_VERSION_BASE 7 -#pragma db model version(DB_SCHEMA_VERSION_BASE, 16, closed) +#pragma db model version(DB_SCHEMA_VERSION_BASE, 17, closed) namespace bpkg { @@ -592,6 +592,13 @@ namespace bpkg const dependency_alternatives_ex&, const package_name&); + // Return true if some clause that is a buildfile fragment is specified for + // any of the dependencies. + // + template + bool + has_buildfile_clause (const vector& dependencies); + // tests // #pragma db value(test_dependency) definition @@ -1109,6 +1116,16 @@ namespace bpkg // optional manifest_checksum; + // Absent if the package has no buildfile clauses in the dependencies. + // Otherwise, the checksum of the buildfiles calculated over the *-build + // manifest values or, if unspecified, the files in the package source + // directory. + // + // Note that for external packages the checksum is always calculated over + // the files. This is "parallel" to the package skeleton logic. + // + optional buildfiles_checksum; + // Path to the output directory of this package, if any. It is // always relative to the configuration directory, and is // for external packages and - for others. It is @@ -1210,6 +1227,7 @@ namespace bpkg optional sr, bool ps, optional mc, + optional bc, optional o, package_prerequisites pps) : name (move (n)), @@ -1224,6 +1242,7 @@ namespace bpkg src_root (move (sr)), purge_src (ps), manifest_checksum (move (mc)), + buildfiles_checksum (move (bc)), out_root (move (o)), prerequisites (move (pps)) {} diff --git a/bpkg/package.ixx b/bpkg/package.ixx index a870eb8..9c85407 100644 --- a/bpkg/package.ixx +++ b/bpkg/package.ixx @@ -11,4 +11,23 @@ namespace bpkg version (v) { } + + template + inline bool + has_buildfile_clause (const vector& ds) + { + for (const dependency_alternatives& das: ds) + { + for (const dependency_alternative& da: das) + { + // Note: the accept clause cannot be present if the prefer clause is + // absent. + // + if (da.enable || da.reflect || da.prefer || da.require) + return true; + } + } + + return false; + } } diff --git a/bpkg/package.xml b/bpkg/package.xml index 7126cf3..d2006f9 100644 --- a/bpkg/package.xml +++ b/bpkg/package.xml @@ -1,4 +1,10 @@ + + + + + + diff --git a/bpkg/pkg-build.cxx b/bpkg/pkg-build.cxx index 24b928a..e77b21b 100644 --- a/bpkg/pkg-build.cxx +++ b/bpkg/pkg-build.cxx @@ -8008,7 +8008,7 @@ namespace bpkg // Commits the transaction. // - sp = pkg_unpack (o, pdb, t, ap->id.name, simulate); + sp = pkg_unpack (o, pdb, af.database (), t, ap->id.name, simulate); if (result) text << "unpacked " << *sp << pdb; diff --git a/bpkg/pkg-checkout.cxx b/bpkg/pkg-checkout.cxx index 61561e2..f2c373d 100644 --- a/bpkg/pkg-checkout.cxx +++ b/bpkg/pkg-checkout.cxx @@ -169,6 +169,7 @@ namespace bpkg auto_rmdir rmd; optional mc; + optional bc; const dir_path& ord (output_root ? *output_root : c); dir_path d (ord / dir_path (n.string () + '-' + v.string ())); @@ -311,6 +312,16 @@ namespace bpkg bspec); mc = package_checksum (o, d, nullptr /* package_info */); + + // Calculate the buildfiles checksum if the package has any buildfile + // clauses in the dependencies. + // + if ((p != nullptr && p->manifest_checksum == mc) + ? p->buildfiles_checksum.has_value () + : has_buildfile_clause (ap->dependencies)) + bc = package_buildfiles_checksum (ap->bootstrap_build, + ap->root_build, + d); } if (p != nullptr) @@ -350,6 +361,7 @@ namespace bpkg p->src_root = move (d); p->purge_src = purge; p->manifest_checksum = move (mc); + p->buildfiles_checksum = move (bc); pdb.update (p); } @@ -370,6 +382,7 @@ namespace bpkg move (d), // Source root. purge, // Purge directory. move (mc), + move (bc), nullopt, // No output directory yet. {}}); // No prerequisites captured yet. diff --git a/bpkg/pkg-configure.cxx b/bpkg/pkg-configure.cxx index dc367a7..772cc67 100644 --- a/bpkg/pkg-configure.cxx +++ b/bpkg/pkg-configure.cxx @@ -331,6 +331,7 @@ namespace bpkg nullopt, // No source directory. false, nullopt, // No manifest checksum. + nullopt, // No buildfiles checksum. nullopt, // No output directory. {}}); // No prerequisites. diff --git a/bpkg/pkg-fetch.cxx b/bpkg/pkg-fetch.cxx index 273a121..531bf84 100644 --- a/bpkg/pkg-fetch.cxx +++ b/bpkg/pkg-fetch.cxx @@ -94,6 +94,7 @@ namespace bpkg nullopt, // No source directory yet. false, nullopt, // No manifest checksum. + nullopt, // No buildfiles checksum. nullopt, // No output directory yet. {}}); // No prerequisites captured yet. diff --git a/bpkg/pkg-unpack.cxx b/bpkg/pkg-unpack.cxx index bc3452b..b38a750 100644 --- a/bpkg/pkg-unpack.cxx +++ b/bpkg/pkg-unpack.cxx @@ -58,26 +58,18 @@ namespace bpkg // package object which may replace the existing one. // static shared_ptr - pkg_unpack (const common_options& o, - database& db, + pkg_unpack (database& db, transaction& t, - package_name n, - version v, - const package_info* pi, - dir_path d, - repository_location rl, + package_name&& n, + version&& v, + dir_path&& d, + repository_location&& rl, + shared_ptr&& p, + optional&& mc, + optional&& bc, bool purge, bool simulate) { - tracer trace ("pkg_unpack"); - - tracer_guard tg (db, trace); - - optional mc; - - if (!simulate) - mc = package_checksum (o, d, pi); - // Make the package path absolute and normalized. If the package is inside // the configuration, use the relative path. This way we can move the // configuration around. @@ -87,8 +79,6 @@ namespace bpkg if (d.sub (db.config)) d = d.leaf (db.config); - shared_ptr p (db.find (n)); - if (p != nullptr) { // Clean up the source directory and archive of the package we are @@ -117,6 +107,7 @@ namespace bpkg p->src_root = move (d); p->purge_src = purge; p->manifest_checksum = move (mc); + p->buildfiles_checksum = move (bc); db.update (p); } @@ -135,6 +126,7 @@ namespace bpkg move (d), purge, move (mc), + move (bc), nullopt, // No output directory yet. {}}); // No prerequisites captured yet. @@ -144,7 +136,59 @@ namespace bpkg assert (p->external ()); t.commit (); - return p; + return move (p); + } + + template + static shared_ptr + pkg_unpack (const common_options& o, + database& db, + transaction& t, + package_name n, + version v, + const vector& deps, + const package_info* pi, + dir_path d, + repository_location rl, + bool purge, + bool simulate) + { + tracer trace ("pkg_unpack"); + + tracer_guard tg (db, trace); + + shared_ptr p (db.find (n)); + + optional mc; + optional bc; + + if (!simulate) + { + mc = package_checksum (o, d, pi); + + // Calculate the buildfiles checksum if the package has any buildfile + // clauses in the dependencies. Always calculate it over the buildfiles + // since the package is external. + // + if ((p != nullptr && p->manifest_checksum == mc) + ? p->buildfiles_checksum.has_value () + : has_buildfile_clause (deps)) + bc = package_buildfiles_checksum (nullopt /* bootstrap_build */, + nullopt /* root_build */, + d); + } + + return pkg_unpack (db, + t, + move (n), + move (v), + move (d), + move (rl), + move (p), + move (mc), + move (bc), + purge, + simulate); } shared_ptr @@ -207,6 +251,7 @@ namespace bpkg t, move (m.name), move (m.version), + m.dependencies, &pvi.info, d, repository_location (), @@ -272,6 +317,7 @@ namespace bpkg t, move (n), move (v), + ap->dependencies, nullptr /* package_info */, path_cast (rl.path () / pl->location), rl, @@ -282,6 +328,7 @@ namespace bpkg shared_ptr pkg_unpack (const common_options& co, database& db, + database& rdb, transaction& t, const package_name& name, bool simulate) @@ -309,13 +356,17 @@ namespace bpkg // Also, since we must have verified the archive during fetch, // here we can just assume what the resulting directory will be. // - dir_path d (c / dir_path (p->name.string () + '-' + p->version.string ())); + const package_name& n (p->name); + const version& v (p->version); + + dir_path d (c / dir_path (n.string () + '-' + v.string ())); if (exists (d)) fail << "package directory " << d << " already exists"; auto_rmdir arm; optional mc; + optional bc; if (!simulate) { @@ -347,12 +398,52 @@ namespace bpkg } mc = package_checksum (co, d, nullptr /* package_info */); + + // Calculate the buildfiles checksum if the package has any buildfile + // clauses in the dependencies. + // + // Note that we may not have the available package (e.g., fetched as an + // existing package archive rather than from an archive repository), in + // which case we need to parse the manifest to retrieve the + // dependencies. This is unfortunate, but is probably not a big deal + // performance-wise given that this is not too common and we are running + // an archive unpacking process anyway. + // + shared_ptr ap ( + rdb.find (available_package_id (n, v))); + + if (ap != nullptr) + { + if (has_buildfile_clause (ap->dependencies)) + bc = package_buildfiles_checksum (ap->bootstrap_build, + ap->root_build, + d); + } + else + { + // Note that we don't need to translate the package version here since + // the manifest comes from an archive and so has a proper version + // already. + // + package_manifest m ( + pkg_verify (co, + d, + true /* ignore_unknown */, + false /* load_buildfiles */, + function ())); + + if (has_buildfile_clause (m.dependencies)) + bc = package_buildfiles_checksum (m.bootstrap_build, + m.root_build, + d); + } } p->src_root = d.leaf (); // For now assuming to be in configuration. p->purge_src = true; p->manifest_checksum = move (mc); + p->buildfiles_checksum = move (bc); p->state = package_state::unpacked; @@ -416,7 +507,12 @@ namespace bpkg // "unpack" it from the directory-based repository. // p = v.empty () - ? pkg_unpack (o, db, t, n, false /* simulate */) + ? pkg_unpack (o, + db /* pdb */, + db /* rdb */, + t, + n, + false /* simulate */) : pkg_unpack (o, db /* pdb */, db /* rdb */, diff --git a/bpkg/pkg-unpack.hxx b/bpkg/pkg-unpack.hxx index 7394732..99d74e0 100644 --- a/bpkg/pkg-unpack.hxx +++ b/bpkg/pkg-unpack.hxx @@ -32,9 +32,13 @@ namespace bpkg // Unpack the fetched package and commit the transaction. // + // Note that both package and repository information configurations need to + // be passed. + // shared_ptr pkg_unpack (const common_options&, - database&, + database& pdb, + database& rdb, transaction&, const package_name&, bool simulate); diff --git a/tests/pkg-build.testscript b/tests/pkg-build.testscript index 711b5b8..30bbe96 100644 --- a/tests/pkg-build.testscript +++ b/tests/pkg-build.testscript @@ -3672,109 +3672,264 @@ test.options += --no-progress : external-package : + if! $remote { - $clone_cfg; + +$clone_cfg - tar (!$posix ? --force-local : ) -xf $src/t8a/fax-1.0.0.tar.gz &fax-1.0.0/***; - mv fax-1.0.0 fax; + +tar (!$posix ? --force-local : ) -xf $src/t8a/fax-1.0.0.tar.gz &fax-1.0.0/*** + +mv fax-1.0.0 fax - $* config.fax.libbiz=true -- fax/ 2>>~"%EOE%"; - fetched $backend_dep - unpacked $backend_dep - fetched libbiz/1.0.0 - unpacked libbiz/1.0.0 - using fax/1.0.0 \(external\) - configured $backend_dep - configured libbiz/1.0.0 - configured fax/1.0.0 - %info: .+fax.+ is up to date% - updated fax/1.0.0 - EOE + : change-manifest + : + { + $clone_cfg; + cp -rp ../fax/ ./; + + $* config.fax.libbiz=true -- fax/ 2>>~"%EOE%"; + fetched $backend_dep + unpacked $backend_dep + fetched libbiz/1.0.0 + unpacked libbiz/1.0.0 + using fax/1.0.0 \(external\) + configured $backend_dep + configured libbiz/1.0.0 + configured fax/1.0.0 + %info: .+fax.+ is up to date% + updated fax/1.0.0 + EOE - $pkg_status -r >>"EOO"; - !fax configured !1.0.0 - $backend_configured - libbiz configured 1.0.0 - EOO + $pkg_status -r >>"EOO"; + !fax configured !1.0.0 + $backend_configured + libbiz configured 1.0.0 + EOO - cat cfg/fax/build/config.build >>~"%EOO%"; - %.* - config.fax.backend = $backend - config.fax.libbiz = true - %.* - EOO + cat cfg/fax/build/config.build >>~"%EOO%"; + %.* + config.fax.backend = $backend + config.fax.libbiz = true + %.* + EOO - # Upgrade the external package after changing its manifest and make - # sure the configuration is preserved. - # - echo '' >+fax/manifest; - - $* fax/ 2>>~%EOE%; - disfigured fax/1.0.0 - using fax/1.0.0#1 (external) - configured fax/1.0.0#1 - %info: .+fax.+ is up to date% - updated fax/1.0.0#1 - EOE + # Upgrade the external package after changing its manifest and make + # sure the configuration is preserved. + # + echo '' >+fax/manifest; + + $* fax/ 2>>~%EOE%; + disfigured fax/1.0.0 + using fax/1.0.0#1 (external) + configured fax/1.0.0#1 + %info: .+fax.+ is up to date% + updated fax/1.0.0#1 + EOE - $pkg_status -r >>"EOO"; - !fax configured !1.0.0#1 - $backend_configured - libbiz configured 1.0.0 - EOO + $pkg_status -r >>"EOO"; + !fax configured !1.0.0#1 + $backend_configured + libbiz configured 1.0.0 + EOO - cat cfg/fax/build/config.build >>~"%EOO%"; - %.* - config.fax.backend = $backend - config.fax.libbiz = true - %.* - EOO + cat cfg/fax/build/config.build >>~"%EOO%"; + %.* + config.fax.backend = $backend + config.fax.libbiz = true + %.* + EOO - # While at it, test that it's ok for out root directory to not exist. - # - # Note that this testing is only meaningful when we replace an - # external package with another external (see build_package::external() - # for details). - # - echo '' >+fax/manifest; - - rm -r cfg/fax/; - - $* fax/ 2>>~%EOE%; - disfigured fax/1.0.0#1 - disfigured libbiz/1.0.0 - purged libbiz/1.0.0 - using fax/1.0.0#2 (external) - configured fax/1.0.0#2 - %info: .+fax.+ is up to date% - updated fax/1.0.0#2 - EOE + # While at it, test that it's ok for out root directory to not + # exist. + # + # Note that this testing is only meaningful when we replace an + # external package with another external (see + # build_package::external() for details). + # + echo '' >+fax/manifest; + + rm -r cfg/fax/; + + $* fax/ 2>>~%EOE%; + disfigured fax/1.0.0#1 + disfigured libbiz/1.0.0 + purged libbiz/1.0.0 + using fax/1.0.0#2 (external) + configured fax/1.0.0#2 + %info: .+fax.+ is up to date% + updated fax/1.0.0#2 + EOE - $pkg_status -r >>"EOO"; - !fax configured !1.0.0#2 - $backend_configured - EOO + $pkg_status -r >>"EOO"; + !fax configured !1.0.0#2 + $backend_configured + EOO - cat cfg/fax/build/config.build >>~"%EOO%"; - %.* - config.fax.backend = $backend - config.fax.libbiz = false - %.* - EOO + cat cfg/fax/build/config.build >>~"%EOO%"; + %.* + config.fax.backend = $backend + config.fax.libbiz = false + %.* + EOO - # Also tests that the depends value location is printed on the enable - # condition evaluation failure for an external package. - # - sed -i -e 's/(depends: libbiz).+/\1 ? (config.fax.libbiz = true)/' fax/manifest; + # Also tests that the depends value location is printed on the + # enable condition evaluation failure for an external package. + # + sed -i -e 's/(depends: libbiz).+/\1 ? (config.fax.libbiz = true)/' fax/manifest; - $* fax/ 2>>~%EOE% != 0; - :1: error: invalid bool value: multiple names - info: enable condition: (config.fax.libbiz = true) - % .+fax.manifest:10:10: info: depends value defined here% - info: while satisfying fax/1.0.0#3 - EOE + $* fax/ 2>>~%EOE% != 0; + :1: error: invalid bool value: multiple names + info: enable condition: (config.fax.libbiz = true) + % .+fax.manifest:10:10: info: depends value defined here% + info: while satisfying fax/1.0.0#3 + EOE - $pkg_drop fax + $pkg_drop fax + } + + : change-buildfile + : + { + +$clone_cfg + + : package-directory + : + { + $clone_cfg; + cp -rp ../../fax/ ./; + + $* fax 2>>~"%EOE%"; + fetched $backend_dep + unpacked $backend_dep + fetched fax/1.0.0 + unpacked fax/1.0.0 + configured $backend_dep + configured fax/1.0.0 + %info: .+fax-1.0.0.+ is up to date% + updated fax/1.0.0 + EOE + + $pkg_status -r >>"EOO"; + !fax configured 1.0.0 + $backend_configured + EOO + + # No upgrade after turning a package from the archive-based repo + # into an external package. + # + $* fax/ 2>>~%EOE%; + %info: .+fax-1.0.0.+ is up to date% + updated fax/1.0.0 + EOE + + $pkg_status -r >>"EOO"; + !fax configured !1.0.0 + $backend_configured + EOO + + # Upgrade after the package' buildfile is edited. + # + echo '' >+fax/build/root.build; + + $* fax/ 2>>~%EOE%; + disfigured fax/1.0.0 + using fax/1.0.0#1 (external) + configured fax/1.0.0#1 + %info: .+fax.+ is up to date% + updated fax/1.0.0#1 + EOE + + $pkg_status -r >>"EOO"; + !fax configured !1.0.0#1 + $backend_configured + EOO + + # No upgrade if the buildfile is not edited. + # + $* fax/ 2>>~%EOE%; + %info: .+fax.+ is up to date% + updated fax/1.0.0#1 + EOE + + $pkg_status -r >>"EOO"; + !fax configured !1.0.0#1 + $backend_configured + EOO + + $pkg_drop fax + } + + : directory-repo + : + { + $clone_cfg; + cp -rp ../../fax/ ./; + + $* fax 2>>~"%EOE%"; + fetched $backend_dep + unpacked $backend_dep + fetched fax/1.0.0 + unpacked fax/1.0.0 + configured $backend_dep + configured fax/1.0.0 + %info: .+fax-1.0.0.+ is up to date% + updated fax/1.0.0 + EOE + + $pkg_status -r >>"EOO"; + !fax configured 1.0.0 + $backend_configured + EOO + + # No upgrade after turning a package from the archive-based repo + # into an external package. + # + $rep_add --type dir fax/ && $rep_fetch; + + $* fax 2>>~%EOE%; + %info: .+fax-1.0.0.+ is up to date% + updated fax/1.0.0 + EOE + + $pkg_status -r >>"EOO"; + !fax configured 1.0.0 + $backend_configured + EOO + + # Upgrade after the package' buildfile is edited. + # + echo '' >+fax/build/root.build; + + $rep_fetch; + + $* fax 2>>~%EOE%; + disfigured fax/1.0.0 + using fax/1.0.0#1 (external) + configured fax/1.0.0#1 + %info: .+fax.+ is up to date% + updated fax/1.0.0#1 + EOE + + $pkg_status -r >>"EOO"; + !fax configured 1.0.0#1 + $backend_configured + EOO + + # No upgrade if the buildfile is not edited. + # + $rep_fetch; + + $* fax 2>>~%EOE%; + %info: .+fax.+ is up to date% + updated fax/1.0.0#1 + EOE + + $pkg_status -r >>"EOO"; + !fax configured 1.0.0#1 + $backend_configured + EOO + + $pkg_drop fax + } + } } : evaluate-reflect-vars -- cgit v1.1