|
| 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 |
0 commit comments