载入中...
搜索中...
未找到
Package.cpp
浏览该文件的文档.
1#include "cmdline.h"
2#include "zip_writer.h"
5#include "common/config.h"
6#include <algorithm>
7#include <filesystem>
8#include <fstream>
9#include <map>
10#include <set>
11#include <sstream>
12#include <CLI11.hpp>
13#include <rang.hpp>
14
15#if defined(EVENGINE_WINDOWS) || defined(_WIN32)
16#include <windows.h>
17#else
18#include <unistd.h>
19#endif
20
21using namespace std::filesystem;
22using namespace std;
23
24namespace eve::cmd
25{
26
27namespace {
28
29// Runtime binary name inside the *target* SDK's bin/. The packaged game runs on
30// the SDK's platform, which is not necessarily the host running `eve package`
31// (e.g. a Linux host can produce a Windows game folder for the win32 SDK).
32std::string targetRuntimeName(const std::string& sdkRoot) {
33 std::ifstream in(std::filesystem::path(sdkRoot) / "share" / "eve" / "TARGET_PLATFORM");
34 std::string plat;
35 std::getline(in, plat);
36 while (!plat.empty() && (plat.back() == '\r' || plat.back() == '\n' || plat.back() == ' '))
37 plat.pop_back();
38 return plat == "win32" ? "eve.exe" : "eve";
39}
40
41bool copyFileIf(const path& src, const path& dst) {
42 error_code ec;
43 if (!exists(src, ec)) return false;
44 create_directories(dst.parent_path(), ec);
45 copy_file(src, dst, copy_options::overwrite_existing, ec);
46 return !ec;
47}
48
49// Best-effort copy of a DLL from any of the candidate source directories.
50bool copyFirst(const std::string& dll, const std::vector<path>& candidates, const path& dstDir) {
51 for (const auto& dir : candidates) {
52 path p = dir / dll;
53 if (copyFileIf(p, dstDir / dll)) return true;
54 }
55 return false;
56}
57
58struct ScriptPackageManifest {
59 std::map<std::string, std::vector<std::string>> graph;
60 std::string error;
61
62 std::string serialize() const {
63 std::ostringstream out;
64 out << "version=1\n";
65 for (const auto& [module, dependencies] : graph) {
66 out << module;
67 for (const std::string& dependency : dependencies) out << '\t' << dependency;
68 out << '\n';
69 }
70 return out.str();
71 }
72};
73
74bool canonicalGameModule(std::string_view importer, std::string_view specifier, std::string& result,
75 std::string& error) {
76 script::ScriptModuleRequest request{std::string(importer), std::string(specifier)};
77 if (!script::ScriptModuleResolver::canonicalize(request, result, error)) return false;
78 if (result.rfind("game:/", 0) != 0) {
79 error = "package cannot statically resolve non-game script module: " + result;
80 return false;
81 }
82 return true;
83}
84
85ScriptPackageManifest scanScriptPackage(const path& gameDir) {
86 ScriptPackageManifest manifest;
87 std::error_code ec;
88 for (recursive_directory_iterator it(gameDir, directory_options::skip_permission_denied, ec), end; it != end;
89 it.increment(ec)) {
90 if (ec) {
91 ec.clear();
92 continue;
93 }
94 if (!it->is_regular_file() || it->path().extension() != ".nut") continue;
95 std::ifstream input(it->path(), std::ios::binary);
96 if (!input) {
97 manifest.error = "cannot read script during package scan: " + it->path().string();
98 return manifest;
99 }
100 const std::string source(std::istreambuf_iterator<char>(input), {});
101 const std::string relative = std::filesystem::relative(it->path(), gameDir, ec).generic_string();
102 if (ec) {
103 manifest.error = "cannot canonicalize packaged script: " + it->path().string();
104 return manifest;
105 }
106 const std::string canonical = "game:/" + relative;
107 auto& edges = manifest.graph[canonical];
108 for (const std::string& specifier : script::ScriptCompiler::analyze(source, canonical).imports) {
109 std::string dependency;
110 if (!canonicalGameModule(canonical, specifier, dependency, manifest.error)) return manifest;
111 edges.push_back(std::move(dependency));
112 }
113 std::sort(edges.begin(), edges.end());
114 edges.erase(std::unique(edges.begin(), edges.end()), edges.end());
115 }
116
117 for (const auto& [module, dependencies] : manifest.graph) {
118 for (const std::string& dependency : dependencies) {
119 if (manifest.graph.find(dependency) == manifest.graph.end()) {
120 manifest.error = "script module imported by " + module + " is missing from package: " + dependency;
121 return manifest;
122 }
123 }
124 }
125
126 enum class Visit { Visiting, Ready };
127 std::map<std::string, Visit> visits;
128 std::vector<std::string> stack;
129 const auto visit = [&](const auto& self, const std::string& module) -> bool {
130 if (const auto found = visits.find(module); found != visits.end()) {
131 if (found->second == Visit::Ready) return true;
132 manifest.error = "cyclic script import in package";
133 for (const std::string& entry : stack) manifest.error += " -> " + entry;
134 manifest.error += " -> " + module;
135 return false;
136 }
137 visits[module] = Visit::Visiting;
138 stack.push_back(module);
139 for (const std::string& dependency : manifest.graph[module]) {
140 if (!self(self, dependency)) return false;
141 }
142 stack.pop_back();
143 visits[module] = Visit::Ready;
144 return true;
145 };
146 for (const auto& [module, _] : manifest.graph) {
147 if (!visit(visit, module)) return manifest;
148 }
149 return manifest;
150}
151
152// Locate the target SDK root: --sdk, EVENGINE_SDK, or next to the running binary.
153std::string resolveSdkRoot(const std::string& sdkArg) {
154 if (!sdkArg.empty() && is_directory(sdkArg)) return sdkArg;
155
156 const char* env = getenv("EVENGINE_SDK");
157 if (env && is_directory(env)) return env;
158
159 // Running from <sdk>/bin/<runtime> -> root is two directories up.
160 path exe;
161#if defined(EVENGINE_WINDOWS) || defined(_WIN32)
162 wchar_t buf[MAX_PATH + 1] = {0};
163 if (GetModuleFileNameW(nullptr, buf, MAX_PATH) != 0) exe = path(buf);
164#else
165 char buf[4096] = {0};
166 const ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
167 if (n > 0) exe = path(std::string(buf, static_cast<size_t>(n)));
168#endif
169 if (!exe.empty()) {
170 path root = exe.parent_path().parent_path();
171 if (exists(root / "share" / "eve" / "TARGET_PLATFORM")) return root.string();
172 }
173 return "";
174}
175
176} // namespace
177
180 void setup(CLI::App& app, std::shared_ptr<CLI::Formatter> formatter) override {
181 auto subcmd = app.add_subcommand("package", "Package a game into a runnable game folder.");
182 subcmd->allow_extras();
183 subcmd->add_option("-o,--output", outputPath, "output folder path");
184 subcmd->add_option("--sdk", sdkPath, "target SDK root (default: auto-detect / $EVENGINE_SDK)");
185 }
186
187 int parse(CLI::App& app, Cmdline& cmd) override {
188 auto subcmd = app.get_subcommand("package");
189 if (subcmd->parsed()) {
190 string game = cmd.get_remaining(subcmd, ".");
191 int res = cmd.Package(game, outputPath, sdkPath);
192 return res;
193 }
194 return -1; // not handle
195 }
196};
197
199
200int Cmdline::Package(std::string gamePath, std::string output, std::string sdk) {
201 error_code ec;
202 if (!is_directory(gamePath, ec)) {
203 cerr << rang::fg::red << "Not a directory: " << rang::fg::reset << gamePath << endl;
204 return 1;
205 }
206
207 std::string sdkRoot = resolveSdkRoot(sdk);
208 if (sdkRoot.empty()) {
209 cerr << rang::fg::red
210 << "Cannot locate the target SDK. Pass --sdk <dir>, set $EVENGINE_SDK, "
211 "or run from <sdk>/bin/<runtime>."
212 << rang::fg::reset << endl;
213 return 2;
214 }
215
216 const ScriptPackageManifest scripts = scanScriptPackage(gamePath);
217 if (!scripts.error.empty()) {
218 cerr << rang::fg::red << "Script dependency scan failed: " << scripts.error << rang::fg::reset << endl;
219 return 5;
220 }
221
222 // Game folder name, trailing separators stripped.
223 std::string gameName = gamePath;
224 while (!gameName.empty() && (gameName.back() == '/' || gameName.back() == '\\'))
225 gameName.pop_back();
226 if (gameName.find_first_of("/\\") != string::npos)
227 gameName = std::filesystem::path(gameName).filename().string();
228 if (gameName.empty()) gameName = "game";
229
230 path outDir = output.empty() ? path(gameName + "-package") : path(output);
231 create_directories(outDir, ec);
232
233 path sdkBin = path(sdkRoot) / "bin";
234 path sdkLib = path(sdkRoot) / "lib";
235 path sdkPlat = path(sdkRoot) / "platform";
236
237 const std::string runtimeName = targetRuntimeName(sdkRoot);
238
239 // 1. Runtime executable.
240 path runtimeSrc = sdkBin / runtimeName;
241 path runtimeDst = outDir / runtimeName;
242 if (!copyFileIf(runtimeSrc, runtimeDst)) {
243 cerr << rang::fg::red << "SDK missing " << runtimeSrc.string() << rang::fg::reset << endl;
244 return 3;
245 }
246
247 // 2. Windows runtime DLLs the packaged game needs (vulkan loader + VC CRT).
248 // win32 SDKs bundle them in bin/ so ANY host can produce a self-contained
249 // package; on a Windows host we also fall back to the local VS redist for
250 // older SDKs that predate bundling.
251 if (runtimeName == "eve.exe") {
252 std::vector<path> crtCandidates;
253 if (exists(sdkBin)) crtCandidates.push_back(sdkBin);
254 if (exists(sdkLib)) crtCandidates.push_back(sdkLib);
255#if defined(EVENGINE_WINDOWS) || defined(_WIN32)
256 // VC redist (version-independent glob): both release CRT and debug CRT.
257 for (const auto& msroot : {"C:/Program Files/Microsoft Visual Studio"}) {
258 path base = path(msroot);
259 if (exists(base / "18")) base = base / "18";
260 for (const auto& entry : directory_iterator(base, ec)) {
261 if (!entry.is_directory()) continue;
262 path redist = entry.path() / "Community" / "VC" / "Redist" / "MSVC";
263 if (!exists(redist)) continue;
264 for (const auto& ver : directory_iterator(redist, ec)) {
265 for (const auto& crtDir : {"x64", "debug_nonredist/x64"}) {
266 path crt = ver.path() / crtDir;
267 if (!exists(crt)) continue;
268 for (const auto& sub : directory_iterator(crt, ec)) {
269 if (!sub.is_directory()) continue;
270 const std::string n = sub.path().filename().string();
271 if (n.find("Microsoft.VC") == 0 &&
272 (n.find(".CRT") != string::npos ||
273 n.find("DebugCRT") != string::npos))
274 crtCandidates.push_back(sub.path());
275 }
276 }
277 }
278 }
279 }
280 if (exists(path("C:/Windows/System32")))
281 crtCandidates.push_back(path("C:/Windows/System32"));
282#endif
283
284 // eve.exe links the VC runtime dynamically; bundle both release and
285 // debug sets so the package is self-contained regardless of build type.
286 for (const auto& dll : {"vulkan-1.dll", "msvcp140.dll", "msvcp140_1.dll",
287 "msvcp140_2.dll", "vcruntime140.dll",
288 "vcruntime140_1.dll", "concrt140.dll",
289 "msvcp140d.dll", "vcruntime140d.dll",
290 "vcruntime140_1d.dll", "ucrtbased.dll",
291 "concrt140d.dll"}) {
292 if (!copyFirst(dll, crtCandidates, outDir))
293 cerr << rang::fg::yellow
294 << "note: runtime DLL not found (may be system-installed): "
295 << dll << rang::fg::reset << endl;
296 }
297 }
298
299#ifdef EVENGINE_MACOSX
300 // 2b. macOS: copy the SDK's lib/ tree (Vulkan loader + MoltenVK + ICD
301 // manifest + zlib/PNG dylibs) into <out>/lib. eve carries INSTALL_RPATH
302 // "@loader_path/../lib", and platform/macosx bootstrapBundledVulkan points
303 // SDL at <out>/lib/libvulkan.1.dylib + <out>/lib/MoltenVK_icd.json, so the
304 // packaged game is self-contained next to the eve binary. Copying the tree
305 // (not just *.dylib) keeps the ICD manifest and any subdirectories.
306 if (exists(sdkLib)) {
307 std::ifstream platIn(std::filesystem::path(sdkRoot) / "share" / "eve" / "TARGET_PLATFORM");
308 std::string plat;
309 std::getline(platIn, plat);
310 while (!plat.empty() && (plat.back() == '\r' || plat.back() == '\n' || plat.back() == ' '))
311 plat.pop_back();
312 // Only macOS-target packages get the dylib tree; cross-packaged
313 // win32/linux games must stay clean.
314 if (plat == "macosx")
315 copy(sdkLib, outDir / "lib",
316 copy_options::recursive | copy_options::overwrite_existing, ec);
317 ec.clear();
318 }
319#endif // EVENGINE_MACOSX
320
321 // 3. Platform packaging template (win32/linux README etc).
322 if (exists(sdkPlat)) {
323 for (const auto& entry : directory_iterator(sdkPlat, ec)) {
324 copy(entry.path(), outDir / entry.path().filename(),
325 copy_options::recursive | copy_options::overwrite_existing, ec);
326 ec.clear();
327 }
328 }
329
330 // 4. Compress the game directory into game.eve inside the package.
331 path archive = outDir / "game.eve";
332 if (!cmdline::createGameArchive(gamePath, archive,
333 {{".eve/script-modules.manifest", scripts.serialize()}})) {
334 cerr << rang::fg::red << "Failed to package game archive." << rang::fg::reset << endl;
335 return 4;
336 }
337
338 cout << rang::fg::green << "Built game package -> " << rang::fg::reset << outDir.string() << endl;
339 cout << " run: " << runtimeDst.string() << endl;
340 return 0;
341}
342
343} // namespace eve::cmd
344
int root
Definition AnimSmr.cpp:119
Vec3 relative
Definition AnimSmr.cpp:164
std::string output
glm::vec4 p[6]
EvpackChunkInput input
Definition Evpack.cpp:170
const GltfImportRequest & request
glm::vec3 n
Definition Grass.cpp:63
bool exists
std::string error
Definition Package.cpp:60
std::map< std::string, std::vector< std::string > > graph
Definition Package.cpp:59
std::string script
Definition PlayHost.cpp:116
std::string path
Definition PlayHost.cpp:110
bool found
V3 dir
Definition TreeMesh.cpp:150
const UnitySourceAsset & source
std::vector< int > edges
命令行模块(eve.cmd):run / build / package / test / zip / dev-server 等子命令入口。
Definition cmdline.h:33
static std::string get_remaining(CLI::App *sub, std::string default_path=".")
子命令剩余位置参数。
Definition cmdline.cpp:80
int Package(std::string path, std::string output, std::string sdk)
把游戏打包为单个可执行文件 / APK。
Definition Package.cpp:200
static bool canonicalize(const ScriptModuleRequest &request, std::string &output, std::string &error)
Normalizes a logical module URI and rejects root traversal.
#define CMD_REG(name)
Definition cmdline.h:101
bool createGameArchive(const std::filesystem::path &gameDir, const std::filesystem::path &outArchive, const std::vector< std::pair< std::string, std::string > > &generatedEntries={})
Creates game archive.
Definition zip_writer.h:220
One eve <subcommand> handler: CLI setup + argument parsing.
Definition cmdline.h:23
void setup(CLI::App &app, std::shared_ptr< CLI::Formatter > formatter) override
Registers options on the CLI11 sub-app.
Definition Package.cpp:180
int parse(CLI::App &app, Cmdline &cmd) override
Parses arguments and runs the command; returns the exit code.
Definition Package.cpp:187