Skip to content

Commit b03a78c

Browse files
committed
0.14.0: deps-archive places an archive's tree beside the program; deps-vcpkg and deps-cmake deploy named files of their prefix
1 parent 25e5c11 commit b03a78c

17 files changed

Lines changed: 512 additions & 15 deletions

File tree

‎.github/scripts/check-deps-and-qt.sh‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ vcpkg_consumer() {
6565
"$MCPP" run | tee target/ci/run.log
6666
grep -qE '^vcpkg-consumer: fmt [0-9]+ says 42$' target/ci/run.log || fail "the program did not print through fmt"
6767
assert_not_rerun "$(stamp_of deps-vcpkg)"
68+
# A file of the prefix the build program named in options::deploy.
69+
find target -path '*/bin/licenses/fmt/copyright' | grep -q . ||
70+
fail "share/fmt/copyright was not deployed beside the program"
71+
echo "ok: a file of the prefix is deployed beside the program"
6872

6973
if is_windows; then
7074
# The pack collects it from the runtime search directory.
@@ -74,6 +78,7 @@ vcpkg_consumer() {
7478
fail "started from the build directory, the program did not find fmt.dll"
7579
"$MCPP" pack --format dir | tee target/ci/pack.log
7680
find target/dist -iname 'fmt.dll' | grep -q . || fail "the packed tree carries no fmt.dll"
81+
find target/dist -path '*/licenses/fmt/copyright' | grep -q . || fail "the packed tree carries no deployed copyright"
7782
echo "ok: the packed tree carries fmt.dll"
7883
fi
7984
}
@@ -128,6 +133,56 @@ cmake_consumer() {
128133
echo "ok: a file added to the subproject re-ran its installation"
129134
}
130135

136+
archive_consumer() {
137+
cd "$ROOT/tests/archive-consumer"
138+
rm -rf target
139+
mkdir -p target/ci
140+
# PLANNING EXTRACTS NOTHING. The listing is read from the archive's central
141+
# directory; the extraction is an action.
142+
"$MCPP" emit build-database --format json > target/ci/db.json 2> target/ci/emit.log ||
143+
{ cat target/ci/emit.log; fail "emit build-database failed before any extraction"; }
144+
[ -z "$(find target -path '*deps-archive*' -type f)" ] || fail "emit build-database extracted something"
145+
echo "ok: emit planned the archive and extracted nothing"
146+
147+
"$MCPP" build 2>&1 | tee target/ci/build.log
148+
"$MCPP" run | tee target/ci/run.log
149+
grep -q "^archive-consumer: 'greetings from the archive', 'a file two levels down'$" target/ci/run.log ||
150+
fail "the program did not read the archive's files from beside itself"
151+
# The C++ file among the data `#error`s if compiled; the build succeeded,
152+
# and the file is beside the program as data.
153+
find target -path '*/bin/runtime/bundle/not-compiled.cpp' | grep -q . ||
154+
fail "an archive member was not deployed beside the program"
155+
echo "ok: the archive's tree is beside the program, a C++ member included, as data"
156+
157+
local copy
158+
copy=$(find target -path '*deps-archive/bundle/bundle/greeting.txt' | head -1)
159+
[ -n "$copy" ] || fail "no extracted copy under target/"
160+
touch -r "$copy" target/ci/before-second-build
161+
sleep 1
162+
"$MCPP" build --profile dev > target/ci/second-build.log 2>&1 ||
163+
{ cat target/ci/second-build.log; fail "the second build failed"; }
164+
[ -z "$(find "$copy" -newer target/ci/before-second-build)" ] ||
165+
fail "a second build with nothing changed extracted the archive again"
166+
echo "ok: a second build with nothing changed did not extract the archive again"
167+
168+
touch assets/bundle.zip
169+
"$MCPP" build --profile dev > target/ci/third-build.log 2>&1 ||
170+
{ cat target/ci/third-build.log; fail "the build after touching the archive failed"; }
171+
[ -n "$(find "$copy" -newer target/ci/before-second-build)" ] ||
172+
fail "a changed archive was not extracted again"
173+
echo "ok: a changed archive was extracted again"
174+
175+
"$MCPP" pack --format dir > target/ci/pack.log 2>&1 || { cat target/ci/pack.log; fail "mcpp pack failed"; }
176+
find target/dist -path '*/runtime/bundle/nested/deep.txt' | grep -q . ||
177+
fail "the packed tree does not carry the archive's files"
178+
local packed
179+
packed=$(find target/dist -path '*/bin/archive-consumer*' -type f | grep -vE '\.(pdb|debug)$' | head -1)
180+
[ -n "$packed" ] || fail "no packed program"
181+
"$packed" | grep -q "^archive-consumer: 'greetings from the archive'" ||
182+
fail "the packed program did not read the archive's files"
183+
echo "ok: the packed tree carries the archive's files, and the packed program reads them"
184+
}
185+
131186
qt_consumer() {
132187
cd "$ROOT/tests/qt-consumer"
133188
rm -rf target
@@ -179,9 +234,10 @@ qt_widgets_consumer() {
179234

180235
case "${1:-}" in
181236
vcpkg-consumer) vcpkg_consumer ;;
237+
archive-consumer) archive_consumer ;;
182238
vcpkg-workspace) vcpkg_workspace ;;
183239
cmake-consumer) cmake_consumer ;;
184240
qt-consumer) qt_consumer ;;
185241
qt-widgets-consumer) qt_widgets_consumer ;;
186-
*) echo "usage: $0 vcpkg-consumer|vcpkg-workspace|cmake-consumer|qt-consumer|qt-widgets-consumer"; exit 2 ;;
242+
*) echo "usage: $0 vcpkg-consumer|archive-consumer|vcpkg-workspace|cmake-consumer|qt-consumer|qt-widgets-consumer"; exit 2 ;;
187243
esac

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,6 +1650,9 @@ jobs:
16501650
- name: deps-vcpkg installs a manifest as an action, and planning installs nothing
16511651
run: bash .github/scripts/check-deps-and-qt.sh vcpkg-consumer
16521652

1653+
- name: deps-archive extracts a zip the project keeps and deploys its tree beside the program
1654+
run: bash .github/scripts/check-deps-and-qt.sh archive-consumer
1655+
16531656
- name: deps-vcpkg in two workspace members that share no dependency
16541657
run: bash .github/scripts/check-deps-and-qt.sh vcpkg-workspace
16551658

@@ -2325,6 +2328,9 @@ jobs:
23252328
- name: deps-vcpkg installs a manifest as an action, and planning installs nothing
23262329
run: bash .github/scripts/check-deps-and-qt.sh vcpkg-consumer
23272330

2331+
- name: deps-archive extracts a zip the project keeps and deploys its tree beside the program
2332+
run: bash .github/scripts/check-deps-and-qt.sh archive-consumer
2333+
23282334
- name: deps-vcpkg in two workspace members that share no dependency
23292335
run: bash .github/scripts/check-deps-and-qt.sh vcpkg-workspace
23302336

‎README.md‎

Lines changed: 49 additions & 9 deletions
Large diffs are not rendered by default.

‎deps/archive.cppm‎

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// mcpp.deps.archive -- files a program reads at run time, from an archive the
2+
// project keeps.
3+
//
4+
// import mcpp.deps.archive;
5+
//
6+
// mcpp::deps::archive::options o;
7+
// o.archive = "assets/python-embed.zip";
8+
// o.to = "runtime"; // beside the program
9+
// if (!mcpp::deps::archive::unpack(o)) return 1;
10+
//
11+
// The archive is extracted by an action, and every file it holds is placed
12+
// beside the program (`mcpp::deploy`): `mcpp run` finds the files there, and
13+
// `mcpp pack` carries them. A project that lays out a directory of its own
14+
// takes `result::files`, the extracted copies, as the inputs of its own
15+
// actions.
16+
//
17+
// THE FILE NAMES ARE READ WHEN THE BUILD PROGRAM RUNS, THE CONTENT ARRIVES WHEN
18+
// THE ACTION DOES. A zip archive lists its members in a central directory at
19+
// its end, which is read without extracting anything; the action therefore
20+
// names each file it writes as an output (SPEC-007 R3.2), and each output can
21+
// be deployed. The archive is a file of the project, so reading its listing is
22+
// configuration, not a construction result (R1.3); the listing is read again
23+
// when the archive changes.
24+
//
25+
// Only zip is listed: a compressed tar has no index, and naming its members
26+
// would mean decompressing it while planning. The extraction itself is CMake's
27+
// `-E tar`, from the `xim:cmake` payload this feature declares.
28+
29+
export module mcpp.deps.archive;
30+
31+
import std;
32+
import mcpp;
33+
import mcpp.plugins;
34+
import mcpp.deps;
35+
36+
export namespace mcpp::deps::archive {
37+
38+
struct options {
39+
// The archive, relative to the package root. A `.zip`.
40+
std::string archive;
41+
// The directory beside the program the archive's tree is placed in. Empty
42+
// or `.` is the program's own directory.
43+
std::string to;
44+
// Names the action and the directory the archive is extracted into. Empty
45+
// takes the archive's file name without its extension.
46+
std::string name;
47+
// The `cmake` executable. Empty is the `xim:cmake` payload.
48+
std::string cmake;
49+
};
50+
51+
struct result {
52+
// One entry per file of the archive: the extracted copy and its path
53+
// beside the program.
54+
std::vector<mcpp::deps::deployed_file> files;
55+
bool ok = false;
56+
explicit operator bool() const { return ok; }
57+
};
58+
59+
// The member names of a zip archive, directories left out, or an error. Reads
60+
// the end-of-central-directory record and the central directory, nothing
61+
// else; a ZIP64 archive (4 GiB and beyond, or 65 535 members and beyond) is
62+
// refused by name rather than misread.
63+
inline std::expected<std::vector<std::string>, std::string>
64+
zip_members(const std::filesystem::path& file) {
65+
std::ifstream in(file, std::ios::binary);
66+
if (!in) return std::unexpected("cannot be opened");
67+
in.seekg(0, std::ios::end);
68+
const auto size = static_cast<std::uint64_t>(in.tellg());
69+
if (size < 22) return std::unexpected("is not a zip archive (too short)");
70+
const std::uint64_t tail = std::min<std::uint64_t>(size, 22 + 65535);
71+
std::string buf(tail, '\0');
72+
in.seekg(static_cast<std::streamoff>(size - tail));
73+
in.read(buf.data(), static_cast<std::streamsize>(tail));
74+
75+
auto u16 = [](const std::string& b, std::size_t at) {
76+
return static_cast<std::uint32_t>(static_cast<unsigned char>(b[at])) |
77+
static_cast<std::uint32_t>(static_cast<unsigned char>(b[at + 1])) << 8;
78+
};
79+
auto u32 = [&](const std::string& b, std::size_t at) {
80+
return u16(b, at) | u16(b, at + 2) << 16;
81+
};
82+
83+
std::size_t eocd = std::string::npos;
84+
for (std::size_t i = tail - 22 + 1; i-- > 0; )
85+
if (u32(buf, i) == 0x06054b50u) { eocd = i; break; }
86+
if (eocd == std::string::npos) return std::unexpected("is not a zip archive (no central directory)");
87+
88+
const std::uint32_t count = u16(buf, eocd + 10);
89+
const std::uint32_t cdSize = u32(buf, eocd + 12);
90+
const std::uint32_t cdOff = u32(buf, eocd + 16);
91+
if (count == 0xFFFFu || cdSize == 0xFFFFFFFFu || cdOff == 0xFFFFFFFFu)
92+
return std::unexpected("is a ZIP64 archive, which this member does not list");
93+
if (static_cast<std::uint64_t>(cdOff) + cdSize > size)
94+
return std::unexpected("has a central directory beyond its end");
95+
96+
std::string cd(cdSize, '\0');
97+
in.seekg(cdOff);
98+
in.read(cd.data(), static_cast<std::streamsize>(cdSize));
99+
if (!in) return std::unexpected("cannot be read");
100+
101+
std::vector<std::string> names;
102+
std::size_t at = 0;
103+
for (std::uint32_t n = 0; n < count; ++n) {
104+
if (at + 46 > cd.size() || u32(cd, at) != 0x02014b50u)
105+
return std::unexpected("has a malformed central directory");
106+
const std::size_t nameLen = u16(cd, at + 28), extraLen = u16(cd, at + 30),
107+
commentLen = u16(cd, at + 32);
108+
if (at + 46 + nameLen > cd.size()) return std::unexpected("has a malformed central directory");
109+
std::string name = cd.substr(at + 46, nameLen);
110+
at += 46 + nameLen + extraLen + commentLen;
111+
for (std::size_t i = 0; i < name.size(); ++i) if (name[i] == '\\') name[i] = '/';
112+
if (name.empty() || name.back() == '/') continue; // a directory
113+
names.push_back(std::move(name));
114+
}
115+
return names;
116+
}
117+
118+
inline result unpack(const options& opt) {
119+
namespace fs = std::filesystem;
120+
constexpr std::string_view who = "mcpp.deps.archive";
121+
mcpp::fact("mcpp.plugins", std::string(mcpp::plugins::version).c_str());
122+
123+
// THE ARCHIVE. A missing or unreadable one is a mistake in the project,
124+
// not a state of the machine, so it is refused.
125+
std::error_code ec;
126+
const fs::path archive = mcpp::deps::absolute_from_root(opt.archive);
127+
if (opt.archive.empty() || !fs::is_regular_file(archive, ec)) {
128+
std::cerr << std::format("{}: options::archive must name a file; got '{}'.\n", who, opt.archive);
129+
return {};
130+
}
131+
mcpp::rerun_if_changed(mcpp::deps::generic(archive).c_str());
132+
auto members = zip_members(archive);
133+
if (!members) {
134+
std::cerr << std::format("{}: {} {}.\n", who, mcpp::deps::generic(archive), members.error());
135+
return {};
136+
}
137+
// A member that would land outside the extraction directory is refused:
138+
// the path it names is the path it is written to.
139+
for (auto const& m : *members) {
140+
const fs::path rel = fs::path(m).lexically_normal();
141+
if (rel.is_absolute() || rel.has_root_name() || rel.empty() || *rel.begin() == "..") {
142+
std::cerr << std::format("{}: {} holds '{}', which leaves the directory it is "
143+
"extracted into.\n", who, mcpp::deps::generic(archive), m);
144+
return {};
145+
}
146+
}
147+
148+
const std::string name = opt.name.empty() ? archive.stem().string() : opt.name;
149+
const fs::path into = fs::path(mcpp::out_dir()) / "deps-archive" / name;
150+
const std::string dir = opt.to.empty() ? std::string(".") : opt.to;
151+
152+
result r;
153+
r.ok = true;
154+
for (auto const& m : *members) {
155+
const fs::path rel = fs::path(m).lexically_normal();
156+
r.files.push_back({mcpp::deps::generic(into / rel),
157+
mcpp::deps::generic(fs::path(dir) / rel)});
158+
}
159+
160+
// ── the tool ──
161+
std::string cmake;
162+
if (!opt.cmake.empty()) {
163+
cmake = mcpp::deps::generic(mcpp::deps::absolute_from_root(opt.cmake));
164+
} else if (const std::string root = mcpp::xpkg_dir("xim", "cmake"); !root.empty()) {
165+
const bool win = std::string(mcpp::host()).find("windows") != std::string::npos;
166+
for (auto const& sub : { fs::path("bin"), fs::path("CMake.app") / "Contents" / "bin" }) {
167+
const auto exe = fs::path(root) / sub / (win ? "cmake.exe" : "cmake");
168+
if (fs::is_regular_file(exe, ec)) { cmake = mcpp::deps::generic(exe); break; }
169+
}
170+
}
171+
if (cmake.empty()) {
172+
// Nothing is extracted, so nothing can be deployed: a deployed file
173+
// must be an output of some action.
174+
mcpp::deps::warn(std::format(
175+
"{}: cmake is not installed (xpkg_dir(\"xim\", \"cmake\") answered \"{}\"), so this plan "
176+
"extracts nothing from {}. The `deps-archive` feature declares `xim:cmake`; "
177+
"`mcpp build` provisions it before this program runs.",
178+
who, std::string(mcpp::xpkg_dir("xim", "cmake")), mcpp::deps::generic(archive)));
179+
r.files.clear();
180+
return r;
181+
}
182+
const std::string tool = mcpp::deps::launcher(who, "deps-archive");
183+
if (tool.empty()) return {};
184+
185+
// ── the extraction, as an edge ──
186+
const std::string id = "deps-archive:" + name;
187+
const std::string desc = "UNPACK " + archive.filename().string();
188+
const std::string arc = mcpp::deps::generic(archive);
189+
const std::string dst = mcpp::deps::generic(into);
190+
mcpp::action a;
191+
a.id = id.c_str();
192+
a.role = mcpp::roles::artifact;
193+
a.description = desc.c_str();
194+
a.arg(tool.c_str()).arg("unpack")
195+
.arg("--cmake").arg(cmake.c_str())
196+
.arg("--archive").arg(arc.c_str())
197+
.arg("--into").arg(dst.c_str())
198+
.input(tool.c_str()).input(cmake.c_str()).input(arc.c_str());
199+
for (auto const& f : r.files) a.output(f.path.c_str());
200+
a.submit();
201+
202+
// ── each file, beside the program ──
203+
for (auto const& f : r.files) {
204+
const std::string parent = fs::path(f.to).parent_path().generic_string();
205+
mcpp::deploy(f.path.c_str(), parent.empty() ? "." : parent.c_str());
206+
}
207+
return r;
208+
}
209+
210+
} // namespace mcpp::deps::archive

‎deps/cmake.cppm‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,16 @@ struct options {
6565
bool shared = false;
6666
// The `cmake` executable. Empty is the `xim:cmake` payload.
6767
std::string cmake;
68+
// Files of the prefix placed beside the program, as `mcpp.deps.vcpkg`
69+
// takes them (`{"bin/tool.cfg", "."}`).
70+
std::vector<mcpp::deps::deploy_entry> deploy;
6871
};
6972

7073
// The prefix, by name (SPEC-007 R1.3).
7174
struct prefix {
7275
std::string root, include, lib, bin;
76+
// The copies `options::deploy` produced, for a project's own layout.
77+
std::vector<mcpp::deps::deployed_file> deployed;
7378
explicit operator bool() const { return !root.empty(); }
7479
};
7580

@@ -145,11 +150,13 @@ inline prefix use(const options& opt) {
145150
}
146151
for (auto const& x : opt.cache_args) a.arg(x.c_str());
147152
a.input(tool.c_str());
153+
a.input(cmake.c_str());
148154
for (auto const& f : mcpp::deps::files_under(source)) a.input(f.c_str());
149155
mcpp::deps::watch_tree(source);
150156
a.output(stamp.c_str());
151157
a.output_dir(p.root.c_str());
152158
a.submit();
159+
p.deployed = mcpp::deps::deploy_after("deps-cmake-" + name, stamp, fs::path(p.root), opt.deploy);
153160
}
154161

155162
mcpp::include_dir(p.include.c_str());

0 commit comments

Comments
 (0)