5#include "common/config.h"
15#if defined(EVENGINE_WINDOWS) || defined(_WIN32)
21using namespace std::filesystem;
32std::string targetRuntimeName(
const std::string& sdkRoot) {
33 std::ifstream in(std::filesystem::path(sdkRoot) /
"share" /
"eve" /
"TARGET_PLATFORM");
35 std::getline(in, plat);
36 while (!plat.empty() && (plat.back() ==
'\r' || plat.back() ==
'\n' || plat.back() ==
' '))
38 return plat ==
"win32" ?
"eve.exe" :
"eve";
41bool copyFileIf(
const path& src,
const path& dst) {
43 if (!
exists(src, ec))
return false;
44 create_directories(dst.parent_path(), ec);
45 copy_file(src, dst, copy_options::overwrite_existing, ec);
50bool copyFirst(
const std::string& dll,
const std::vector<path>& candidates,
const path& dstDir) {
51 for (
const auto&
dir : candidates) {
53 if (copyFileIf(
p, dstDir / dll))
return true;
58struct ScriptPackageManifest {
59 std::map<std::string, std::vector<std::string>>
graph;
62 std::string serialize()
const {
63 std::ostringstream out;
65 for (
const auto& [module, dependencies] :
graph) {
67 for (
const std::string& dependency : dependencies) out <<
'\t' << dependency;
74bool canonicalGameModule(std::string_view importer, std::string_view specifier, std::string& result,
76 script::ScriptModuleRequest
request{std::string(importer), std::string(specifier)};
78 if (result.rfind(
"game:/", 0) != 0) {
79 error =
"package cannot statically resolve non-game script module: " + result;
85ScriptPackageManifest scanScriptPackage(
const path& gameDir) {
86 ScriptPackageManifest manifest;
88 for (recursive_directory_iterator it(gameDir, directory_options::skip_permission_denied, ec),
end; it !=
end;
94 if (!it->is_regular_file() || it->path().extension() !=
".nut")
continue;
95 std::ifstream
input(it->path(), std::ios::binary);
97 manifest.error =
"cannot read script during package scan: " + it->path().string();
100 const std::string
source(std::istreambuf_iterator<char>(
input), {});
101 const std::string
relative = std::filesystem::relative(it->path(), gameDir, ec).generic_string();
103 manifest.error =
"cannot canonicalize packaged script: " + it->path().string();
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));
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;
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;
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;
143 visits[module] = Visit::Ready;
146 for (
const auto& [module, _] : manifest.
graph) {
147 if (!visit(visit, module))
return manifest;
153std::string resolveSdkRoot(
const std::string& sdkArg) {
154 if (!sdkArg.empty() && is_directory(sdkArg))
return sdkArg;
156 const char* env = getenv(
"EVENGINE_SDK");
157 if (env && is_directory(env))
return env;
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);
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)));
170 path root = exe.parent_path().parent_path();
171 if (
exists(
root /
"share" /
"eve" /
"TARGET_PLATFORM"))
return root.string();
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)");
188 auto subcmd = app.get_subcommand(
"package");
189 if (subcmd->parsed()) {
202 if (!is_directory(gamePath, ec)) {
203 cerr << rang::fg::red <<
"Not a directory: " << rang::fg::reset << gamePath << endl;
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;
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;
223 std::string gameName = gamePath;
224 while (!gameName.empty() && (gameName.back() ==
'/' || gameName.back() ==
'\\'))
226 if (gameName.find_first_of(
"/\\") != string::npos)
227 gameName = std::filesystem::path(gameName).filename().string();
228 if (gameName.empty()) gameName =
"game";
231 create_directories(outDir, ec);
233 path sdkBin =
path(sdkRoot) /
"bin";
234 path sdkLib =
path(sdkRoot) /
"lib";
235 path sdkPlat =
path(sdkRoot) /
"platform";
237 const std::string runtimeName = targetRuntimeName(sdkRoot);
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;
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)
257 for (
const auto& msroot : {
"C:/Program Files/Microsoft Visual Studio"}) {
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());
281 crtCandidates.push_back(
path(
"C:/Windows/System32"));
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",
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;
299#ifdef EVENGINE_MACOSX
307 std::ifstream platIn(std::filesystem::path(sdkRoot) /
"share" /
"eve" /
"TARGET_PLATFORM");
309 std::getline(platIn, plat);
310 while (!plat.empty() && (plat.back() ==
'\r' || plat.back() ==
'\n' || plat.back() ==
' '))
314 if (plat ==
"macosx")
315 copy(sdkLib, outDir /
"lib",
316 copy_options::recursive | copy_options::overwrite_existing, ec);
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);
331 path archive = outDir /
"game.eve";
333 {{
".eve/script-modules.manifest", scripts.serialize()}})) {
334 cerr << rang::fg::red <<
"Failed to package game archive." << rang::fg::reset << endl;
338 cout << rang::fg::green <<
"Built game package -> " << rang::fg::reset << outDir.string() << endl;
339 cout <<
" run: " << runtimeDst.string() << endl;
const GltfImportRequest & request
std::map< std::string, std::vector< std::string > > graph
const UnitySourceAsset & source
命令行模块(eve.cmd):run / build / package / test / zip / dev-server 等子命令入口。
static std::string get_remaining(CLI::App *sub, std::string default_path=".")
子命令剩余位置参数。
int Package(std::string path, std::string output, std::string sdk)
把游戏打包为单个可执行文件 / APK。
static bool canonicalize(const ScriptModuleRequest &request, std::string &output, std::string &error)
Normalizes a logical module URI and rejects root traversal.
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.
One eve <subcommand> handler: CLI setup + argument parsing.
void setup(CLI::App &app, std::shared_ptr< CLI::Formatter > formatter) override
Registers options on the CLI11 sub-app.
int parse(CLI::App &app, Cmdline &cmd) override
Parses arguments and runs the command; returns the exit code.