30std::string lower(std::string
s) {
32 if (
c >=
'A' &&
c <=
'Z')
c =
static_cast<char>(
c -
'A' +
'a');
40 return static_cast<int>(getpid());
45bool nonEmptyRegularFile(
const std::filesystem::path&
path, std::error_code& ec) {
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;
53bool zipEntryPathSafe(
const std::string&
name) {
54 if (
name.empty())
return false;
55 if (
name[0] ==
'/' ||
name[0] ==
'\\')
return false;
57 for (
const char c :
name) {
58 if (
c ==
'/' ||
c ==
'\\') {
59 if (comp ==
".." || comp ==
".")
return false;
65 return comp !=
".." && comp !=
".";
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;
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));
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));
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) {
96 if (i == searchStart)
break;
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;
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;
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);
122 const uint32_t extAttr = le32(entryStart + 38);
126 if (!zipEntryPathSafe(
name))
return false;
127 const bool isDir = !
name.empty() &&
name.back() ==
'/';
128 const auto dest = std::filesystem::path(destDir) /
name;
131 std::filesystem::create_directories(dest, ec);
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)
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()))
143 std::vector<char> out;
145 out.assign(data.begin() +
static_cast<ptrdiff_t
>(lData),
146 data.begin() +
static_cast<ptrdiff_t
>(lData + compSz));
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);
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) {
162 out.insert(out.end(), buf.begin(), buf.end() - zs.avail_out);
163 }
while (ret != Z_STREAM_END);
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)
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;
183 const auto mode =
static_cast<unsigned>((extAttr >> 16) & 0xFFFF);
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);
199 const std::string
n = lower(
name);
215 default:
return "unknown";
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;
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;
244 const std::string
override =
getEnv(
"EVE_SDK_INSTALL_ROOT");
245 if (!
override.empty())
return override;
247 std::string base =
getEnv(
"LOCALAPPDATA");
248 if (base.empty()) base =
getEnv(
"USERPROFILE");
249 return base.empty() ? std::string(
"EVEngine/sdk") : base +
"/EVEngine/sdk";
251 std::string base =
getEnv(
"XDG_DATA_HOME");
253 const std::string home =
getEnv(
"HOME");
254 base = home.empty() ? std::string(
"") : home +
"/.local/share";
256 return base.empty() ? std::string(
"EVEngine/sdk") : base +
"/EVEngine/sdk";
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/") +
268 std::ifstream in(
path, std::ios::binary);
276 if (in.bad())
return "";
282bool isSdkRoot(
const std::filesystem::path&
dir) {
284 return std::filesystem::is_regular_file(
285 std::filesystem::path(
dir) /
"share" /
"eve" /
"TARGET_PLATFORM", 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();
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();
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();
309 char buf[4096] = {0};
310 const ssize_t
n = readlink(
"/proc/self/exe", buf,
sizeof(buf) - 1);
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();
317 const auto cwd = std::filesystem::current_path(ec);
318 if (!ec && isSdkRoot(cwd))
return cwd.string();
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)) {
331 if (!it->is_directory(ec) || !isSdkRoot(it->path()))
continue;
333 if (plat ==
"android")
return it->path().string();
334 if (fallback.empty()) fallback = it->path().string();
343std::string sha256ForName(
const std::string& sums,
const std::string&
name) {
344 std::istringstream iss(sums);
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'))
356 if (rest ==
name)
return lower(hex);
362std::string sdkVersion(
const std::string&
root) {
363 std::ifstream in(std::filesystem::path(
root) /
"share" /
"eve" /
"VERSION");
366 while (!
s.empty() && (
s.back() ==
'\r' ||
s.back() ==
'\n' ||
s.back() ==
' '))
s.pop_back();
371bool moveOrCopy(
const std::filesystem::path&
from,
const std::filesystem::path&
to);
376 namespace fs = std::filesystem;
381 if (
tag.empty() ||
tag[0] !=
'v') {
385 const std::string expectedVer =
tag.substr(1);
387 const std::string destRoot = (fs::path(installRoot) / plat).
string();
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;
399 const std::string zipName =
"eve-sdk-" + plat +
"-" +
tag +
".zip";
400 const std::string zipUrl = base +
"/" + zipName;
401 const std::string sumsUrl = base +
"/SHA256SUMS";
403 fs::temp_directory_path(ec) / (
"eve-get-" + plat +
"-" + std::to_string(processId()));
408 fs::remove_all(work, ec);
409 fs::create_directories(work, ec);
414 const std::string zipPath = (work / zipName).
string();
415 const std::string sumsPath = (work /
"SHA256SUMS").
string();
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);
423 if (!nonEmptyRegularFile(zipPath, ec)) {
424 fs::remove_all(work, ec);
428 if (
runShell(
"curl -fL --retry 3 -o \"" + sumsPath +
"\" \"" + sumsUrl +
"\"") != 0) {
429 fs::remove_all(work, ec);
433 if (!nonEmptyRegularFile(sumsPath, ec)) {
434 fs::remove_all(work, ec);
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);
451 const std::string extractDir = (work /
"extract").
string();
452 fs::create_directories(extractDir, ec);
454 fs::remove_all(work, ec);
458 if (!extractZip(zipPath, extractDir)) {
459 fs::remove_all(work, ec);
463 const auto srcRoot = fs::path(extractDir) /
"eve-sdk" / plat;
464 if (!fs::is_directory(srcRoot, ec)) {
465 fs::remove_all(work, ec);
471 const std::string verMarker = sdkVersion(srcRoot.string());
472 if (platMarker != plat) {
473 fs::remove_all(work, ec);
477 if (verMarker != expectedVer) {
478 fs::remove_all(work, ec);
484 fs::create_directories(installRoot, ec);
486 fs::remove_all(work, ec);
490 if (!moveOrCopy(srcRoot, fs::path(destRoot))) {
491 fs::remove_all(work, ec);
495 fs::remove_all(work, ec);
496 std::cout <<
"eve get: EVEngine " << plat <<
" SDK " <<
tag <<
" installed at "
497 << destRoot << std::endl;
502 std::ifstream in(std::filesystem::path(sdkRoot) /
"share" /
"eve" /
"TARGET_PLATFORM");
505 while (!
s.empty() && (
s.back() ==
'\r' ||
s.back() ==
'\n' ||
s.back() ==
' '))
s.pop_back();
510 for (
const char*
n : {
"EVENGINE_ANDROID_SDK",
"ANDROID_SDK_ROOT",
"ANDROID_HOME"}) {
512 if (!
v.empty())
return v;
516 if (!
local.empty())
return local +
"/Android/Sdk";
517 return getEnv(
"USERPROFILE") +
"/Android/Sdk";
519 const std::string home =
getEnv(
"HOME");
520 return home.empty() ? std::string(
"Android/Sdk") : home +
"/Android/Sdk";
525 std::ifstream in(
path);
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;
539#if defined(EVENGINE_IOS)
542 std::cerr <<
"eve: shell commands are not available on iOS: " << cmd << std::endl;
545 const int rc = std::system(cmd.c_str());
547 std::cerr <<
"eve: failed to run: " << cmd << std::endl;
553 if (WIFEXITED(rc))
return WEXITSTATUS(rc);
561std::string exeSuffix() {
570std::string sdkmanagerSuffix() {
580std::string androidCmdlineToolsUrl() {
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";
586 return "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip";
592std::string temurinJdk17Url() {
598#elif defined(__APPLE__)
600#if defined(__aarch64__)
607#if defined(__aarch64__)
613 return std::string(
"https://api.adoptium.net/v3/binary/latest/17/ga/") + os +
"/" + arch +
614 "/jdk/hotspot/normal/eclipse";
618const char* kAndroidPlatform =
"platforms;android-34";
619const char* kAndroidBuildTools =
"build-tools;34.0.0";
622const char* kGradleVersion =
"8.5";
624std::string gradleUrl() {
625 return std::string(
"https://services.gradle.org/distributions/gradle-") + kGradleVersion +
629bool jdkHomeExists(
const std::string& home) {
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);
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) {
645 if (!nonEmptyRegularFile(zipPath, ec)) {
649 std::filesystem::remove_all(extractDir, ec);
650 std::filesystem::create_directories(extractDir, ec);
655 if (!extractZip(zipPath, extractDir)) {
659 std::filesystem::remove(zipPath, ec);
668bool moveOrCopy(
const std::filesystem::path&
from,
const std::filesystem::path&
to) {
670 std::filesystem::remove_all(
to, ec);
671 std::filesystem::rename(
from,
to, ec);
672 if (!ec)
return true;
674 std::filesystem::copy(
from,
to,
675 std::filesystem::copy_options::recursive |
676 std::filesystem::copy_options::overwrite_existing,
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);
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();
695 if (ec || jdkDir.empty() || !moveOrCopy(jdkDir, std::filesystem::path(
root) /
"jdk17")) {
699 std::filesystem::remove_all(tmp, ec);
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);
710 const auto gradleDir = std::filesystem::path(
root) / (
"gradle-" + std::string(kGradleVersion));
711 if (!moveOrCopy(std::filesystem::path(tmp) / (
"gradle-" + std::string(kGradleVersion)),
716 std::filesystem::remove_all(tmp, ec);
720bool gradleInstalled(
const std::string& gradleHome) {
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);
732 return std::filesystem::is_regular_file(
733 std::filesystem::path(
root) /
"cmdline-tools" /
"latest" /
"bin" /
734 (
"sdkmanager" + sdkmanagerSuffix()),
746 std::filesystem::create_directories(
root, ec);
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";
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);
763 if (!std::filesystem::is_directory(std::filesystem::path(tmp) /
"cmdline-tools", ec)) {
767 if (!moveOrCopy(std::filesystem::path(tmp) /
"cmdline-tools",
768 std::filesystem::path(
root) /
"cmdline-tools" /
"latest")) {
772 std::filesystem::remove_all(tmp, ec);
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);
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";
787 setEnv(
"JAVA_HOME", javaHome);
790 const std::string lic =
root +
"/.eve-licenses.txt";
792 std::ofstream
f(lic, std::ios::binary | std::ios::trunc);
797 for (
int i = 0; i < 200; ++i)
f <<
"y\n";
806 const std::string sdkCmd =
"call \"" + sdkManager +
"\"";
808 const std::string sdkCmd =
"\"" + sdkManager +
"\"";
810 std::cout <<
"eve get: accepting Android SDK licenses...\n";
811 if (
runShell(sdkCmd +
" --licenses < \"" + lic +
"\"") != 0) {
815 std::cout <<
"eve get: installing SDK packages (platform-tools, android-34, "
817 const std::string packages =
"\"platform-tools\" \"" + std::string(kAndroidPlatform) +
818 "\" \"" + std::string(kAndroidBuildTools) +
"\"";
819 if (
runShell(sdkCmd +
" " + packages) != 0) {
823 std::filesystem::remove(lic, ec);
826 std::string gradleHome = (std::filesystem::path(
root) / (
"gradle-" + std::string(kGradleVersion))).string();
827 if (!gradleInstalled(gradleHome)) {
828 auto gradle = installGradle(
root);
833 const std::string eveSdkRoot =
836 std::ofstream
f(std::filesystem::path(
root) /
"eve-android.env",
837 std::ios::binary | std::ios::trunc);
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";
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";
861void copyOne(
const std::filesystem::path&
from,
const std::filesystem::path&
to,
862 const std::vector<std::string>& skipDirs,
bool contentsOnly) {
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)) {
873 const auto rel = std::filesystem::relative(it->path(),
from, ec).lexically_normal();
875 const std::string relStr = rel.generic_string();
877 for (
const auto&
d : skipDirs) {
878 if (relStr ==
d || relStr.rfind(
d +
"/", 0) == 0) {
884 it.disable_recursion_pending();
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);
903 const std::vector<std::string>& skipDirs) {
905 if (!std::filesystem::is_directory(
from, ec))
return false;
906 copyOne(
from,
to, skipDirs,
false);
912 if (!std::filesystem::is_directory(
from, ec))
return false;
913 std::filesystem::create_directories(
to, ec);
914 copyOne(
from,
to, {},
true);