载入中...
搜索中...
未找到
sdk_tools.cpp
浏览该文件的文档.
1#include "cmdline/sdk_tools.h"
2#include "cmdline/Sha256.h"
3
4#include "common/config.h"
5
6#include <cstdlib>
7#include <filesystem>
8#include <fstream>
9#include <iostream>
10#include <limits>
11#include <sstream>
12
13#include <zlib.h>
14
15#if defined(_WIN32)
16#include <windows.h>
17#include <process.h>
18#else
19#include <unistd.h>
20#endif
21
22#if !defined(_WIN32)
23#include <sys/wait.h>
24#endif
25
26namespace eve::cmd::sdk {
27
28namespace {
29
30std::string lower(std::string s) {
31 for (char& c : s)
32 if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
33 return s;
34}
35
36int processId() {
37#if defined(_WIN32)
38 return _getpid();
39#else
40 return static_cast<int>(getpid());
41#endif
42}
43
44[[nodiscard]]
45bool nonEmptyRegularFile(const std::filesystem::path& path, std::error_code& ec) {
46 ec.clear();
47 if (!std::filesystem::is_regular_file(path, ec) || ec) return false;
48 const auto size = std::filesystem::file_size(path, ec);
49 return !ec && size > 0;
50}
51
52// 校验 zip 条目路径安全:拒绝绝对路径与 ".."/"." 组件(zip-slip 防护)。
53bool zipEntryPathSafe(const std::string& name) {
54 if (name.empty()) return false;
55 if (name[0] == '/' || name[0] == '\\') return false;
56 std::string comp;
57 for (const char c : name) {
58 if (c == '/' || c == '\\') {
59 if (comp == ".." || comp == ".") return false;
60 comp.clear();
61 } else {
62 comp += c;
63 }
64 }
65 return comp != ".." && comp != ".";
66}
67
68// 进程内 ZIP 解压(STORED + DEFLATE):宿主 tar 不一定能读 zip(Linux 的
69// GNU tar 就不支持),而 SDK 安装不能要求用户机器预装 unzip/bsdtar。
70bool extractZip(const std::string& zipPath, const std::string& destDir) {
71 std::ifstream in(zipPath, std::ios::binary);
72 if (!in) return false;
73 const std::vector<char> data((std::istreambuf_iterator<char>(in)),
74 std::istreambuf_iterator<char>());
75 if (data.size() < 22) return false;
76
77 const auto le16 = [&](size_t off) -> uint16_t {
78 return static_cast<uint16_t>(static_cast<unsigned char>(data[off]) |
79 (static_cast<unsigned char>(data[off + 1]) << 8));
80 };
81 const auto le32 = [&](size_t off) -> uint32_t {
82 return static_cast<uint32_t>(static_cast<unsigned char>(data[off]) |
83 (static_cast<unsigned char>(data[off + 1]) << 8) |
84 (static_cast<unsigned char>(data[off + 2]) << 16) |
85 (static_cast<unsigned char>(data[off + 3]) << 24));
86 };
87
88 // EOCD 在文件末尾 64KB+22 字节内。
89 size_t eocd = std::string::npos;
90 const size_t searchStart = data.size() > 65557 ? data.size() - 65557 : 0;
91 for (size_t i = data.size() - 22;; --i) {
92 if (data[i] == 'P' && data[i + 1] == 'K' && data[i + 2] == 5 && data[i + 3] == 6) {
93 eocd = i;
94 break;
95 }
96 if (i == searchStart) break;
97 }
98 if (eocd == std::string::npos) return false;
99 const uint32_t cdOffset = le32(eocd + 16);
100 const uint32_t cdSize = le32(eocd + 12);
101 if (cdOffset > data.size() || cdSize > data.size() - cdOffset) return false;
102
103 size_t pos = cdOffset;
104 const size_t cdEnd = cdOffset + cdSize;
105 while (cdEnd - pos >= 46 && data[pos] == 'P' && data[pos + 1] == 'K' && data[pos + 2] == 1 && data[pos + 3] == 2) {
106 const size_t entryStart = pos;
107 const uint16_t method = le16(pos + 10);
108 const uint32_t entryCrc = le32(pos + 16);
109 const uint32_t compSz = le32(pos + 20);
110 const uint32_t uncompSz = le32(pos + 24);
111 const uint16_t nameLen = le16(pos + 28);
112 const uint16_t extraLen = le16(pos + 30);
113 const uint16_t commentLen = le16(pos + 32);
114 const uint32_t localOff = le32(pos + 42);
115 const size_t entrySize = static_cast<size_t>(46) + nameLen + extraLen + commentLen;
116 if (entrySize > cdEnd - pos) return false;
117 const std::string name(data.data() + entryStart + 46, nameLen);
118 // Read the central-directory attributes before advancing to the next entry.
119 // Reading them from `pos` after the increment was the vector overrun seen
120 // when installing the local release fixture.
121#if !defined(_WIN32)
122 const uint32_t extAttr = le32(entryStart + 38);
123#endif
124 pos += entrySize;
125
126 if (!zipEntryPathSafe(name)) return false;
127 const bool isDir = !name.empty() && name.back() == '/';
128 const auto dest = std::filesystem::path(destDir) / name;
129 std::error_code ec;
130 if (isDir) {
131 std::filesystem::create_directories(dest, ec);
132 continue;
133 }
134 if (localOff > data.size() || data.size() - localOff < 30 || data[localOff] != 'P' ||
135 data[localOff + 1] != 'K' || data[localOff + 2] != 3 || data[localOff + 3] != 4)
136 return false;
137 const size_t localHeaderSize = static_cast<size_t>(30) + le16(localOff + 26) + le16(localOff + 28);
138 if (localHeaderSize > data.size() - localOff) return false;
139 const size_t lData = localOff + localHeaderSize;
140 if (compSz > data.size() - lData || compSz > static_cast<uint32_t>(std::numeric_limits<uInt>::max()))
141 return false;
142
143 std::vector<char> out;
144 if (method == 0) {
145 out.assign(data.begin() + static_cast<ptrdiff_t>(lData),
146 data.begin() + static_cast<ptrdiff_t>(lData + compSz));
147 } else if (method == 8) {
148 z_stream zs = {};
149 if (inflateInit2(&zs, -15) != Z_OK) return false;
150 zs.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(data.data() + lData));
151 zs.avail_in = static_cast<uInt>(compSz);
152 std::vector<char> buf(65536);
153 int ret;
154 do {
155 zs.next_out = reinterpret_cast<Bytef*>(buf.data());
156 zs.avail_out = static_cast<uInt>(buf.size());
157 ret = inflate(&zs, Z_NO_FLUSH);
158 if (ret != Z_OK && ret != Z_STREAM_END) {
159 inflateEnd(&zs);
160 return false;
161 }
162 out.insert(out.end(), buf.begin(), buf.end() - zs.avail_out);
163 } while (ret != Z_STREAM_END);
164 inflateEnd(&zs);
165 } else {
166 return false; // 不支持的方法(zip 里极罕见)。
167 }
168 if (out.size() != uncompSz) return false;
169 if (static_cast<uint32_t>(
170 crc32(0, reinterpret_cast<const Bytef*>(out.data()), static_cast<uInt>(out.size()))) != entryCrc)
171 return false;
172
173 std::filesystem::create_directories(dest.parent_path(), ec);
174 if (ec) return false;
175 std::ofstream ofs(dest, std::ios::binary | std::ios::trunc);
176 if (!ofs) return false;
177 ofs.write(out.data(), static_cast<std::streamsize>(out.size()));
178 if (!ofs) return false;
179#if !defined(_WIN32)
180 // Preserve the unix mode recorded in the zip's external attributes
181 // (bits 16-23) so extracted tools stay executable: gradle, sdkmanager,
182 // aapt/adb all ship as scripts/binaries with +x in their archives.
183 const auto mode = static_cast<unsigned>((extAttr >> 16) & 0xFFFF);
184 if (mode & 0111) {
185 std::error_code pec;
186 std::filesystem::permissions(dest,
187 std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec |
188 std::filesystem::perms::others_exec,
189 std::filesystem::perm_options::add, pec);
190 }
191#endif
192 }
193 return pos == cdEnd;
194}
195
196} // namespace
197
198Platform parsePlatform(const std::string& name) {
199 const std::string n = lower(name);
200 if (n == "android") return Platform::Android;
201 if (n == "ios") return Platform::Ios;
202 if (n == "linux") return Platform::Linux;
203 if (n == "macosx" || n == "macos" || n == "mac" || n == "osx") return Platform::Macosx;
204 if (n == "win32" || n == "windows" || n == "win" || n == "win64") return Platform::Win32;
205 return Platform::Unknown;
206}
207
208std::string platformName(Platform p) {
209 switch (p) {
210 case Platform::Win32: return "win32";
211 case Platform::Linux: return "linux";
212 case Platform::Macosx: return "macosx";
213 case Platform::Android: return "android";
214 case Platform::Ios: return "ios";
215 default: return "unknown";
216 }
217}
218
219std::string getEnv(const std::string& name, const std::string& def) {
220 const char* v = std::getenv(name.c_str());
221 return v ? std::string(v) : def;
222}
223
224void setEnv(const std::string& name, const std::string& value) {
225#if defined(_WIN32)
226 _putenv_s(name.c_str(), value.c_str());
227#else
228 setenv(name.c_str(), value.c_str(), 1);
229#endif
230}
231
232std::string sdkVersionTag() {
233 const std::string override = getEnv("EVE_SDK_TAG");
234 if (!override.empty()) return override;
235 std::string v = EVENGINE_VERSION;
236 const size_t dash = v.find('-');
237 if (dash != std::string::npos) v = v.substr(0, dash);
238 if (v.empty()) return "";
239 if (v[0] != 'v') v = "v" + v;
240 return v;
241}
242
243std::string eveSdkInstallRoot() {
244 const std::string override = getEnv("EVE_SDK_INSTALL_ROOT");
245 if (!override.empty()) return override;
246#if defined(_WIN32)
247 std::string base = getEnv("LOCALAPPDATA");
248 if (base.empty()) base = getEnv("USERPROFILE");
249 return base.empty() ? std::string("EVEngine/sdk") : base + "/EVEngine/sdk";
250#else
251 std::string base = getEnv("XDG_DATA_HOME");
252 if (base.empty()) {
253 const std::string home = getEnv("HOME");
254 base = home.empty() ? std::string("") : home + "/.local/share";
255 }
256 return base.empty() ? std::string("EVEngine/sdk") : base + "/EVEngine/sdk";
257#endif
258}
259
260std::string eveSdkBaseUrl() {
261 const std::string override = getEnv("EVE_SDK_BASE_URL");
262 if (!override.empty()) return override;
263 return std::string("https://github.com/EVEngine/EVEngine/releases/download/") +
265}
266
267std::string fileSha256(const std::string& path) {
268 std::ifstream in(path, std::ios::binary);
269 if (!in) return "";
271 char buffer[16384];
272 while (in) {
273 in.read(buffer, sizeof(buffer));
274 if (in.gcount() > 0) digest.update(buffer, static_cast<std::size_t>(in.gcount()));
275 }
276 if (in.bad()) return "";
277 return digest.finish();
278}
279
280namespace {
281
282bool isSdkRoot(const std::filesystem::path& dir) {
283 std::error_code ec;
284 return std::filesystem::is_regular_file(
285 std::filesystem::path(dir) / "share" / "eve" / "TARGET_PLATFORM", ec);
286}
287
288} // namespace
289
290std::string findSdkRoot(const std::string& sdkArg) {
291 std::error_code ec;
292 if (!sdkArg.empty()) {
293 const auto p = std::filesystem::absolute(sdkArg, ec);
294 if (!ec && std::filesystem::is_directory(p, ec)) return p.string();
295 return "";
296 }
297 const std::string env = getEnv("EVENGINE_SDK");
298 if (!env.empty() && std::filesystem::is_directory(env, ec))
299 return std::filesystem::absolute(env, ec).string();
300
301 // Running from <sdk>/bin/<runtime> -> root is two directories up.
302#if defined(_WIN32)
303 wchar_t buf[MAX_PATH + 1] = {0};
304 if (GetModuleFileNameW(nullptr, buf, MAX_PATH) != 0) {
305 const auto exeDir = std::filesystem::path(buf).parent_path();
306 if (isSdkRoot(exeDir.parent_path())) return exeDir.parent_path().string();
307 }
308#else
309 char buf[4096] = {0};
310 const ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
311 if (n > 0) {
312 const auto exeDir = std::filesystem::path(std::string(buf, static_cast<size_t>(n))).parent_path();
313 if (isSdkRoot(exeDir.parent_path())) return exeDir.parent_path().string();
314 }
315#endif
316
317 const auto cwd = std::filesystem::current_path(ec);
318 if (!ec && isSdkRoot(cwd)) return cwd.string();
319
320 // `eve get <platform>` 安装的 SDK(<installRoot>/<platform>)。优先返回
321 // android(build 目前唯一支持的平台),其次任意 SDK 根。
322 const auto installRoot = std::filesystem::path(eveSdkInstallRoot());
323 std::string fallback;
324 if (std::filesystem::is_directory(installRoot, ec)) {
325 std::filesystem::directory_iterator it(installRoot, ec), end;
326 for (; it != end; it.increment(ec)) {
327 if (ec) {
328 ec.clear();
329 continue;
330 }
331 if (!it->is_directory(ec) || !isSdkRoot(it->path())) continue;
332 const std::string plat = sdkTargetPlatform(it->path().string());
333 if (plat == "android") return it->path().string();
334 if (fallback.empty()) fallback = it->path().string();
335 }
336 }
337 return fallback;
338}
339
340namespace {
341
342// 从 SHA256SUMS 文本里找 "<hex> <name>" 行,返回小写 hex;找不到返回空串。
343std::string sha256ForName(const std::string& sums, const std::string& name) {
344 std::istringstream iss(sums);
345 std::string line;
346 while (std::getline(iss, line)) {
347 const size_t sp = line.find(' ');
348 if (sp == std::string::npos) continue;
349 const std::string hex = line.substr(0, sp);
350 std::string rest = line.substr(sp + 1);
351 while (!rest.empty() && (rest.front() == ' ' || rest.front() == '\t'))
352 rest.erase(rest.begin());
353 while (!rest.empty() &&
354 (rest.back() == '\r' || rest.back() == ' ' || rest.back() == '\t'))
355 rest.pop_back();
356 if (rest == name) return lower(hex);
357 }
358 return "";
359}
360
361// 读取 <root>/share/eve/VERSION;文件缺失或为空时返回空串。
362std::string sdkVersion(const std::string& root) {
363 std::ifstream in(std::filesystem::path(root) / "share" / "eve" / "VERSION");
364 std::string s;
365 std::getline(in, s);
366 while (!s.empty() && (s.back() == '\r' || s.back() == '\n' || s.back() == ' ')) s.pop_back();
367 return s;
368}
369
370// moveOrCopy 定义在本文件较后位置(下载/解压工具区),这里前向声明。
371bool moveOrCopy(const std::filesystem::path& from, const std::filesystem::path& to);
372
373} // namespace
374
376 namespace fs = std::filesystem;
378 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "unknown SDK platform", {}, {}, "cmdline.sdk"));
379 const std::string plat = platformName(p);
380 const std::string tag = sdkVersionTag();
381 if (tag.empty() || tag[0] != 'v') {
383 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "cannot determine the current EVEngine version; set EVE_SDK_TAG", "EVE_SDK_TAG", {}, "cmdline.sdk"));
384 }
385 const std::string expectedVer = tag.substr(1);
386 const std::string installRoot = eveSdkInstallRoot();
387 const std::string destRoot = (fs::path(installRoot) / plat).string();
388 std::error_code ec;
389
390 // 已安装同版本则直接复用,避免重复下载。
391 const std::string currentVer = sdkVersion(destRoot);
392 if (!currentVer.empty() && currentVer == expectedVer) {
393 std::cout << "eve get: EVEngine " << plat << " SDK already installed at "
394 << destRoot << " (" << tag << ")" << std::endl;
396 }
397
398 const std::string base = eveSdkBaseUrl();
399 const std::string zipName = "eve-sdk-" + plat + "-" + tag + ".zip";
400 const std::string zipUrl = base + "/" + zipName;
401 const std::string sumsUrl = base + "/SHA256SUMS";
402 const auto work =
403 fs::temp_directory_path(ec) / ("eve-get-" + plat + "-" + std::to_string(processId()));
404 if (ec) {
406 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "no temporary directory available", {}, {}, "cmdline.sdk"));
407 }
408 fs::remove_all(work, ec);
409 fs::create_directories(work, ec);
410 if (ec) {
412 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "cannot create SDK download workspace: " + ec.message(), work.string(), {}, "cmdline.sdk"));
413 }
414 const std::string zipPath = (work / zipName).string();
415 const std::string sumsPath = (work / "SHA256SUMS").string();
416
417 std::cout << "eve get: downloading " << zipName << " (" << zipUrl << ")...\n";
418 if (runShell("curl -fL --retry 3 -o \"" + zipPath + "\" \"" + zipUrl + "\"") != 0) {
419 fs::remove_all(work, ec);
421 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "SDK archive download failed", zipUrl, {}, "cmdline.sdk"));
422 }
423 if (!nonEmptyRegularFile(zipPath, ec)) {
424 fs::remove_all(work, ec);
426 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "SDK archive download returned an empty or unreadable response", zipUrl, {}, "cmdline.sdk"));
427 }
428 if (runShell("curl -fL --retry 3 -o \"" + sumsPath + "\" \"" + sumsUrl + "\"") != 0) {
429 fs::remove_all(work, ec);
431 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "SDK checksum file download failed", sumsUrl, {}, "cmdline.sdk"));
432 }
433 if (!nonEmptyRegularFile(sumsPath, ec)) {
434 fs::remove_all(work, ec);
436 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "SDK checksum download returned an empty or unreadable response", sumsUrl, {}, "cmdline.sdk"));
437 }
438 {
439 std::ifstream in(sumsPath);
440 const std::string sums(
441 (std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
442 const std::string expected = sha256ForName(sums, zipName);
443 const std::string actual = fileSha256(zipPath);
444 if (expected.empty() || actual.empty() || lower(expected) != lower(actual)) {
445 fs::remove_all(work, ec);
447 eve::Diagnostic::error(eve::DiagnosticCode::HashMismatch, "SDK SHA-256 verification failed for " + zipName, zipPath, {}, "cmdline.sdk"));
448 }
449 }
450
451 const std::string extractDir = (work / "extract").string();
452 fs::create_directories(extractDir, ec);
453 if (ec) {
454 fs::remove_all(work, ec);
456 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "cannot create SDK extraction directory: " + ec.message(), extractDir, {}, "cmdline.sdk"));
457 }
458 if (!extractZip(zipPath, extractDir)) {
459 fs::remove_all(work, ec);
461 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "SDK archive is empty, truncated, or not a supported ZIP archive", zipPath, {}, "cmdline.sdk"));
462 }
463 const auto srcRoot = fs::path(extractDir) / "eve-sdk" / plat;
464 if (!fs::is_directory(srcRoot, ec)) {
465 fs::remove_all(work, ec);
467 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "unexpected SDK archive layout; expected eve-sdk/" + plat + "/", zipPath, {}, "cmdline.sdk"));
468 }
469 {
470 const std::string platMarker = sdkTargetPlatform(srcRoot.string());
471 const std::string verMarker = sdkVersion(srcRoot.string());
472 if (platMarker != plat) {
473 fs::remove_all(work, ec);
475 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "SDK archive targets '" + platMarker + "', not '" + plat + "'", (srcRoot / "share" / "eve" / "TARGET_PLATFORM").string(), {}, "cmdline.sdk"));
476 }
477 if (verMarker != expectedVer) {
478 fs::remove_all(work, ec);
480 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "SDK archive contains version '" + verMarker + "', expected '" + expectedVer + "'", (srcRoot / "share" / "eve" / "VERSION").string(), {}, "cmdline.sdk"));
481 }
482 }
483
484 fs::create_directories(installRoot, ec);
485 if (ec) {
486 fs::remove_all(work, ec);
488 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "cannot create SDK install directory: " + ec.message(), installRoot, {}, "cmdline.sdk"));
489 }
490 if (!moveOrCopy(srcRoot, fs::path(destRoot))) {
491 fs::remove_all(work, ec);
493 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "failed to install the SDK", destRoot, {}, "cmdline.sdk"));
494 }
495 fs::remove_all(work, ec);
496 std::cout << "eve get: EVEngine " << plat << " SDK " << tag << " installed at "
497 << destRoot << std::endl;
499}
500
501std::string sdkTargetPlatform(const std::string& sdkRoot) {
502 std::ifstream in(std::filesystem::path(sdkRoot) / "share" / "eve" / "TARGET_PLATFORM");
503 std::string s;
504 std::getline(in, s);
505 while (!s.empty() && (s.back() == '\r' || s.back() == '\n' || s.back() == ' ')) s.pop_back();
506 return s;
507}
508
509std::string androidSdkRoot() {
510 for (const char* n : {"EVENGINE_ANDROID_SDK", "ANDROID_SDK_ROOT", "ANDROID_HOME"}) {
511 const std::string v = getEnv(n);
512 if (!v.empty()) return v;
513 }
514#if defined(_WIN32)
515 const std::string local = getEnv("LOCALAPPDATA");
516 if (!local.empty()) return local + "/Android/Sdk";
517 return getEnv("USERPROFILE") + "/Android/Sdk";
518#else
519 const std::string home = getEnv("HOME");
520 return home.empty() ? std::string("Android/Sdk") : home + "/Android/Sdk";
521#endif
522}
523
524void applyEnvFile(const std::string& path) {
525 std::ifstream in(path);
526 if (!in) return;
527 std::string line;
528 while (std::getline(in, line)) {
529 if (line.empty() || line[0] == '#') continue;
530 const size_t eq = line.find('=');
531 if (eq == std::string::npos) continue;
532 const std::string key = line.substr(0, eq);
533 if (key.empty()) continue;
534 if (getEnv(key).empty()) setEnv(key, line.substr(eq + 1));
535 }
536}
537
538int runShell(const std::string& cmd) {
539#if defined(EVENGINE_IOS)
540 // iOS forbids system(); the shell-out paths (eve get / eve build) are
541 // desktop-host only and never run on the iOS runtime.
542 std::cerr << "eve: shell commands are not available on iOS: " << cmd << std::endl;
543 return 127;
544#else
545 const int rc = std::system(cmd.c_str());
546 if (rc == -1) {
547 std::cerr << "eve: failed to run: " << cmd << std::endl;
548 return 127;
549 }
550#if defined(_WIN32)
551 return rc;
552#else
553 if (WIFEXITED(rc)) return WEXITSTATUS(rc);
554 return 1;
555#endif
556#endif
557}
558
559namespace {
560
561std::string exeSuffix() {
562#if defined(_WIN32)
563 return ".exe";
564#else
565 return "";
566#endif
567}
568
569// sdkmanager 在 Windows 上是 .bat 脚本,其它平台是无后缀的 sh 脚本。
570std::string sdkmanagerSuffix() {
571#if defined(_WIN32)
572 return ".bat";
573#else
574 return "";
575#endif
576}
577
578// Google 官方 command-line tools 压缩包(按宿主平台)。可用
579// EVE_ANDROID_CMDLINE_TOOLS_URL 覆盖(镜像/测试)。
580std::string androidCmdlineToolsUrl() {
581#if defined(_WIN32)
582 return "https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip";
583#elif defined(__APPLE__)
584 return "https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip";
585#else
586 return "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip";
587#endif
588}
589
590// Eclipse Temurin JDK 17 直链(Adoptium API 重定向到最新 17 GA)。
591// 可用 EVE_JDK17_URL 覆盖(镜像/测试)。
592std::string temurinJdk17Url() {
593 const char* os;
594 const char* arch;
595#if defined(_WIN32)
596 os = "windows";
597 arch = "x64";
598#elif defined(__APPLE__)
599 os = "mac";
600#if defined(__aarch64__)
601 arch = "aarch64";
602#else
603 arch = "x64";
604#endif
605#else
606 os = "linux";
607#if defined(__aarch64__)
608 arch = "aarch64";
609#else
610 arch = "x64";
611#endif
612#endif
613 return std::string("https://api.adoptium.net/v3/binary/latest/17/ga/") + os + "/" + arch +
614 "/jdk/hotspot/normal/eclipse";
615}
616
617// 与仓库 APK 工程匹配的 Android SDK 组件版本(.so 由 SDK 预编译,无需 NDK)。
618const char* kAndroidPlatform = "platforms;android-34";
619const char* kAndroidBuildTools = "build-tools;34.0.0";
620
621// APK 模板的 gradle-wrapper.properties 指定的 Gradle 版本。
622const char* kGradleVersion = "8.5";
623
624std::string gradleUrl() {
625 return std::string("https://services.gradle.org/distributions/gradle-") + kGradleVersion +
626 "-bin.zip";
627}
628
629bool jdkHomeExists(const std::string& home) {
630 std::error_code ec;
631 return std::filesystem::is_regular_file(
632 std::filesystem::path(home) / "bin" / ("java" + exeSuffix()), ec) ||
633 std::filesystem::is_regular_file(
634 std::filesystem::path(home) / "bin" / ("javac" + exeSuffix()), ec);
635}
636
637// 下载一个压缩包并用系统 tar 解压(Windows 10+ 自带 bsdtar)。
638[[nodiscard]] eve::Result<void> downloadAndExtract(const std::string& url, const std::string& zipPath,
639 const std::string& extractDir) {
640 if (runShell("curl -fL --retry 3 -o \"" + zipPath + "\" \"" + url + "\"") != 0) {
642 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "download failed", url, {}, "cmdline.sdk"));
643 }
644 std::error_code ec;
645 if (!nonEmptyRegularFile(zipPath, ec)) {
647 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "download returned an empty or unreadable response", url, {}, "cmdline.sdk"));
648 }
649 std::filesystem::remove_all(extractDir, ec);
650 std::filesystem::create_directories(extractDir, ec);
651 if (ec) {
653 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "cannot create extraction directory: " + ec.message(), extractDir, {}, "cmdline.sdk"));
654 }
655 if (!extractZip(zipPath, extractDir)) {
657 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "downloaded archive is empty, truncated, or unsupported", zipPath, {}, "cmdline.sdk"));
658 }
659 std::filesystem::remove(zipPath, ec);
660 if (ec) {
662 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "cannot remove temporary downloaded archive: " + ec.message(), zipPath, {}, "cmdline.sdk"));
663 }
665}
666
667// move 失败(跨盘符等)时退化为递归 copy。
668bool moveOrCopy(const std::filesystem::path& from, const std::filesystem::path& to) {
669 std::error_code ec;
670 std::filesystem::remove_all(to, ec);
671 std::filesystem::rename(from, to, ec);
672 if (!ec) return true;
673 ec.clear();
674 std::filesystem::copy(from, to,
675 std::filesystem::copy_options::recursive |
676 std::filesystem::copy_options::overwrite_existing,
677 ec);
678 return !ec;
679}
680
681[[nodiscard]] eve::Result<void> installJdk(const std::string& root) {
682 const std::string url = getEnv("EVE_JDK17_URL", temurinJdk17Url());
683 std::cout << "eve get: downloading JDK 17 (Temurin)...\n";
684 const std::string tmp = root + "/.jdk-extract";
685 auto downloaded = downloadAndExtract(url, root + "/jdk17.zip", tmp);
686 if (!downloaded.ok()) return eve::Result<void>::failure(downloaded.status());
687 std::error_code ec;
688 std::filesystem::path jdkDir;
689 for (const auto& entry : std::filesystem::directory_iterator(tmp, ec)) {
690 if (entry.is_directory()) {
691 jdkDir = entry.path();
692 break;
693 }
694 }
695 if (ec || jdkDir.empty() || !moveOrCopy(jdkDir, std::filesystem::path(root) / "jdk17")) {
697 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "unexpected JDK archive layout", tmp, {}, "cmdline.sdk"));
698 }
699 std::filesystem::remove_all(tmp, ec);
701}
702
703[[nodiscard]] eve::Result<void> installGradle(const std::string& root) {
704 const std::string url = getEnv("EVE_GRADLE_URL", gradleUrl());
705 std::cout << "eve get: downloading Gradle " << kGradleVersion << "...\n";
706 const std::string tmp = root + "/.gradle-extract";
707 auto downloaded = downloadAndExtract(url, root + "/gradle.zip", tmp);
708 if (!downloaded.ok()) return eve::Result<void>::failure(downloaded.status());
709 std::error_code ec;
710 const auto gradleDir = std::filesystem::path(root) / ("gradle-" + std::string(kGradleVersion));
711 if (!moveOrCopy(std::filesystem::path(tmp) / ("gradle-" + std::string(kGradleVersion)),
712 gradleDir)) {
714 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "unexpected Gradle archive layout", tmp, {}, "cmdline.sdk"));
715 }
716 std::filesystem::remove_all(tmp, ec);
718}
719
720bool gradleInstalled(const std::string& gradleHome) {
721 std::error_code ec;
722 return std::filesystem::is_regular_file(
723 std::filesystem::path(gradleHome) / "bin" / "gradle", ec) ||
724 std::filesystem::is_regular_file(
725 std::filesystem::path(gradleHome) / "bin" / "gradle.bat", ec);
726}
727
728} // namespace
729
730bool isAndroidSdkInstalled(const std::string& root) {
731 std::error_code ec;
732 return std::filesystem::is_regular_file(
733 std::filesystem::path(root) / "cmdline-tools" / "latest" / "bin" /
734 ("sdkmanager" + sdkmanagerSuffix()),
735 ec);
736}
737
739 // EVEngine 官方 android SDK 优先:预编译 native 库 + APK 模板按当前 eve
740 // 版本从 GitHub Release 下载(校验 SHA256)。
741 auto eveSdk = installEveSdk(Platform::Android);
742 if (!eveSdk.ok()) return eve::Result<void>::failure(eveSdk.status());
743
744 const std::string root = androidSdkRoot();
745 std::error_code ec;
746 std::filesystem::create_directories(root, ec);
747 if (ec) {
749 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "cannot create Android SDK directory: " + ec.message(), root, {}, "cmdline.sdk"));
750 }
751
752 const std::string sdkManager =
753 root + "/cmdline-tools/latest/bin/sdkmanager" + sdkmanagerSuffix();
755 std::cout << "eve get: Android SDK already installed at " << root << "\n";
756 } else {
757 std::cout << "eve get: downloading Android command-line tools...\n";
758 const std::string url =
759 getEnv("EVE_ANDROID_CMDLINE_TOOLS_URL", androidCmdlineToolsUrl());
760 const std::string tmp = root + "/.cmdline-tools-extract";
761 auto downloaded = downloadAndExtract(url, root + "/cmdline-tools.zip", tmp);
762 if (!downloaded.ok()) return eve::Result<void>::failure(downloaded.status());
763 if (!std::filesystem::is_directory(std::filesystem::path(tmp) / "cmdline-tools", ec)) {
765 eve::Diagnostic::error(eve::DiagnosticCode::ParseError, "unexpected Android command-line tools archive layout", tmp, {}, "cmdline.sdk"));
766 }
767 if (!moveOrCopy(std::filesystem::path(tmp) / "cmdline-tools",
768 std::filesystem::path(root) / "cmdline-tools" / "latest")) {
770 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "failed to install Android command-line tools", root + "/cmdline-tools/latest", {}, "cmdline.sdk"));
771 }
772 std::filesystem::remove_all(tmp, ec);
773 }
774
775 // JDK 17 优先复用 $JAVA_HOME;否则下载 Temurin 17 到 <sdk>/jdk17。
776 std::string javaHome = getEnv("JAVA_HOME");
777 if (javaHome.empty()) {
778 javaHome = (std::filesystem::path(root) / "jdk17").string();
779 if (!jdkHomeExists(javaHome)) {
780 auto jdk = installJdk(root);
781 if (!jdk.ok()) return eve::Result<void>::failure(jdk.status());
782 }
783 } else if (!jdkHomeExists(javaHome)) {
784 std::cerr << "eve get: warning: JAVA_HOME=" << javaHome
785 << " does not contain bin/java; make sure it points at a JDK 17+.\n";
786 }
787 setEnv("JAVA_HOME", javaHome);
788
789 // 接受许可并安装组件(sdkmanager 幂等,可重复执行补齐)。
790 const std::string lic = root + "/.eve-licenses.txt";
791 {
792 std::ofstream f(lic, std::ios::binary | std::ios::trunc);
793 if (!f) {
795 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "cannot create Android SDK license input", lic, {}, "cmdline.sdk"));
796 }
797 for (int i = 0; i < 200; ++i) f << "y\n";
798 if (!f) {
800 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "cannot write Android SDK license input", lic, {}, "cmdline.sdk"));
801 }
802 }
803#if defined(_WIN32)
804 // cmd /c strips the first/last quote of a command that starts with one, so
805 // batch files with spaces in their path need the `call` prefix.
806 const std::string sdkCmd = "call \"" + sdkManager + "\"";
807#else
808 const std::string sdkCmd = "\"" + sdkManager + "\"";
809#endif
810 std::cout << "eve get: accepting Android SDK licenses...\n";
811 if (runShell(sdkCmd + " --licenses < \"" + lic + "\"") != 0) {
813 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "failed to accept Android SDK licenses", sdkManager, {}, "cmdline.sdk"));
814 }
815 std::cout << "eve get: installing SDK packages (platform-tools, android-34, "
816 "build-tools)...\n";
817 const std::string packages = "\"platform-tools\" \"" + std::string(kAndroidPlatform) +
818 "\" \"" + std::string(kAndroidBuildTools) + "\"";
819 if (runShell(sdkCmd + " " + packages) != 0) {
821 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "failed to install Android SDK packages", sdkManager, {}, "cmdline.sdk"));
822 }
823 std::filesystem::remove(lic, ec);
824
825 // Gradle:APK 模板没有 gradlew.bat,直接用发行版启动器,无需额外工具。
826 std::string gradleHome = (std::filesystem::path(root) / ("gradle-" + std::string(kGradleVersion))).string();
827 if (!gradleInstalled(gradleHome)) {
828 auto gradle = installGradle(root);
829 if (!gradle.ok()) return eve::Result<void>::failure(gradle.status());
830 }
831
832 // 记录环境,供 `eve build android` 在环境变量未设置时使用。
833 const std::string eveSdkRoot =
834 (std::filesystem::path(eveSdkInstallRoot()) / "android").string();
835 {
836 std::ofstream f(std::filesystem::path(root) / "eve-android.env",
837 std::ios::binary | std::ios::trunc);
838 if (!f) {
840 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "cannot write Android SDK environment file", (std::filesystem::path(root) / "eve-android.env").string(), {}, "cmdline.sdk"));
841 }
842 f << "ANDROID_HOME=" << root << "\n"
843 << "ANDROID_SDK_ROOT=" << root << "\n"
844 << "JAVA_HOME=" << javaHome << "\n"
845 << "GRADLE_HOME=" << gradleHome << "\n"
846 << "EVENGINE_SDK=" << eveSdkRoot << "\n";
847 if (!f) {
849 eve::Diagnostic::error(eve::DiagnosticCode::Failed, "cannot finish writing Android SDK environment file", (std::filesystem::path(root) / "eve-android.env").string(), {}, "cmdline.sdk"));
850 }
851 }
852
853 std::cout << "eve get: Android SDK ready at " << root << "\n"
854 << "eve get: run `eve build android` to build an APK "
855 "(or `eve build -d android` for a debug APK).\n";
857}
858
859namespace {
860
861void copyOne(const std::filesystem::path& from, const std::filesystem::path& to,
862 const std::vector<std::string>& skipDirs, bool contentsOnly) {
863 std::error_code ec;
864 if (!contentsOnly) std::filesystem::create_directories(to, ec);
865 std::filesystem::recursive_directory_iterator it(
866 from, std::filesystem::directory_options::skip_permission_denied, ec);
867 std::filesystem::recursive_directory_iterator end;
868 for (; it != end; it.increment(ec)) {
869 if (ec) {
870 ec.clear();
871 continue;
872 }
873 const auto rel = std::filesystem::relative(it->path(), from, ec).lexically_normal();
874 if (ec) continue;
875 const std::string relStr = rel.generic_string();
876 bool skip = false;
877 for (const auto& d : skipDirs) {
878 if (relStr == d || relStr.rfind(d + "/", 0) == 0) {
879 skip = true;
880 break;
881 }
882 }
883 if (skip) {
884 it.disable_recursion_pending();
885 continue;
886 }
887 // 无论是否 contentsOnly,目标都按相对路径落位(contentsOnly 时 to 就是
888 // 目标根目录,rel 相对源根目录)。
889 const auto dst = to / rel;
890 if (it->is_directory()) {
891 std::filesystem::create_directories(dst, ec);
892 } else if (it->is_regular_file()) {
893 std::filesystem::create_directories(dst.parent_path(), ec);
894 std::filesystem::copy_file(it->path(), dst,
895 std::filesystem::copy_options::overwrite_existing, ec);
896 }
897 }
898}
899
900} // namespace
901
902bool copyTree(const std::string& from, const std::string& to,
903 const std::vector<std::string>& skipDirs) {
904 std::error_code ec;
905 if (!std::filesystem::is_directory(from, ec)) return false;
906 copyOne(from, to, skipDirs, /*contentsOnly=*/false);
907 return true;
908}
909
910bool copyTreeContents(const std::string& from, const std::string& to) {
911 std::error_code ec;
912 if (!std::filesystem::is_directory(from, ec)) return false;
913 std::filesystem::create_directories(to, ec);
914 copyOne(from, to, {}, /*contentsOnly=*/true);
915 return true;
916}
917
918} // namespace eve::cmd::sdk
double value
int root
Definition AnimSmr.cpp:119
std::string from
const std::string & s
glm::vec4 p[6]
std::uint16_t method
std::uint32_t key
glm::vec3 n
Definition Grass.cpp:63
float v
std::int32_t c
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
std::string local
std::string name
const std::string * tag
std::unique_ptr< gpgpu::GpuBuffer > buffer
Definition OnnxGpgpu.cpp:26
float f
std::string path
Definition PlayHost.cpp:110
float d
std::string digest
float size
Definition TreeMesh.cpp:156
V3 dir
Definition TreeMesh.cpp:150
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Sha256 public API.
Definition Sha256.h:11
void update(const char *data, std::size_t size)
Updates .
Definition Sha256.cpp:73
std::string androidSdkRoot()
解析 Android SDK 根目录:EVENGINE_ANDROID_SDK / ANDROID_SDK_ROOT / ANDROID_HOME,最后回落到平台默认位置(Windows: LOCALA...
std::string getEnv(const std::string &name, const std::string &def)
读取环境变量;未设置时返回 def。
std::string findSdkRoot(const std::string &sdkArg)
定位 EVEngine SDK 根目录:–sdk 参数 > $EVENGINE_SDK > 可执行文件 所在位置的 ../(<sdk>/bin/eve)> 当前目录 > eve get 安装的 SDK。
std::string sdkVersionTag()
当前 eve 版本对应的发布 tag(v0.1.0-dev → v0.1.0)。 可用 EVE_SDK_TAG 覆盖(dev 构建 / 测试)。解析失败时返回空串。
bool copyTree(const std::string &from, const std::string &to, const std::vector< std::string > &skipDirs)
递归复制目录,跳过指定名称的子目录(如 build/.gradle)。
Platform
平台集合(与仓库 Makefile 的 build/<platform> 目标一一对应)。
Definition sdk_tools.h:13
bool isAndroidSdkInstalled(const std::string &root)
判断 Android SDK 是否已安装(cmdline-tools 的 sdkmanager 存在)。
int runShell(const std::string &cmd)
通过系统 shell 执行命令并返回退出码(POSIX 下归一化 WEXITSTATUS)。
eve::Result< void > installAndroidSdk()
自动下载并安装 Android 工具链:EVEngine 官方 android SDK + cmdline-tools / platform-tools / build-tools / platform...
std::string eveSdkBaseUrl()
官方 SDK 下载基址(GitHub Release 对应 tag),可用 EVE_SDK_BASE_URL 覆盖(镜像 / 测试,如 file://...)。
std::string fileSha256(const std::string &path)
流式计算文件的 SHA-256(小写十六进制);保留失败返回空串的既有契约。
void setEnv(const std::string &name, const std::string &value)
设置当前进程的环境变量(不持久化到系统)。
std::string platformName(Platform p)
平台规范名(用于 Makefile 目标名)。
void applyEnvFile(const std::string &path)
读取 KEY=VALUE 环境文件;只应用当前进程尚未设置的变量。
std::string eveSdkInstallRoot()
EVEngine 官方 SDK 安装根目录(Windows: LOCALAPPDATA%/EVEngine/sdk, POSIX: $XDG_DATA_HOME/EVEngine/sdk),可用 EVE...
eve::Result< void > installEveSdk(Platform p)
下载并安装 EVEngine 官方 <platform> SDK(zip 内 eve-sdk/<platform>/, 从 GitHub Release 按当前 eve 版本挑选),校验 SHA256 ...
std::string sdkTargetPlatform(const std::string &sdkRoot)
读取 <sdk>/share/eve/TARGET_PLATFORM(该 SDK 面向的目标平台)。
bool copyTreeContents(const std::string &from, const std::string &to)
把 from 目录的内容(不含 from 本身)复制到 to。
Platform parsePlatform(const std::string &name)
解析平台名(大小写不敏感,含常用别名:windows/win、mac/macos/osx 等)。