/* solana_build.h: the Build button (included once by solana.c). * * A game is built on the player's own PC from their own disc: nothing from the * game is downloaded. From the game's GitHub release (RELEASE_URL + the game's * tag, e.g. .../releases/download/rac1/) it fetches: * .json what to fetch: version, files, sizes, SHA-256s * toolchain-win64.zip Python and zig (the compiler); shared by all games, * kept in toolchain\ and fetched again only when its version changes * -build.zip the game's build kit: build.py, the recompilers, the * host engine and the decomp's C, unpacked to games\\ * then runs build.py on the disc image and keeps only what the game needs * (games\\build\windows). The zips are checked against their SHA-256s and * unpacked by the small inflate below (no tar.exe needed). * * solana.ini [build] release_url= and levels= override the * source and the levels (testing, offline installs). */ #include #include #define RELEASE_URL L"https://github.com/OpenRAC/project-solana/releases/download/" #define WM_BUILD_UPDATE (WM_APP + 2) typedef struct { volatile int active, cancel, failed, disc; volatile double frac; /* 0..1 for the bar */ wchar_t status[256]; /* what it is doing */ wchar_t err[512]; /* why it stopped */ HANDLE job; /* the build's processes (closed: they end) */ } build_t; static build_t bld = { 0, 0, 0, -1, 0, {0}, {0}, 0 }; static void bld_post(void) { static DWORD last; DWORD t = GetTickCount(); if (t - last > 80) { last = t; PostMessageW(hwnd, WM_BUILD_UPDATE, 0, 0); } } static void bld_status(double frac, const wchar_t *fmt, ...) { va_list ap; va_start(ap, fmt); _vsnwprintf(bld.status, 255, fmt, ap); bld.status[255] = 0; va_end(ap); if (frac >= 0) bld.frac = frac; bld_post(); } static int bld_fail(const wchar_t *fmt, ...) { va_list ap; va_start(ap, fmt); _vsnwprintf(bld.err, 511, fmt, ap); bld.err[511] = 0; va_end(ap); bld.failed = 1; return 0; } /* ---- paths ---- */ static void path_join(wchar_t *out, const wchar_t *a, const wchar_t *b) { swprintf(out, MAX_PATH, L"%ls\\%ls", a, b); } static void mkdirs(const wchar_t *dir) { wchar_t p[MAX_PATH]; wcsncpy(p, dir, MAX_PATH - 1); p[MAX_PATH - 1] = 0; for (wchar_t *s = p + 3; *s; s++) if (*s == L'\\' || *s == L'/') { wchar_t c = *s; *s = 0; CreateDirectoryW(p, 0); *s = c; } CreateDirectoryW(p, 0); } static void rm_tree(const wchar_t *path) { wchar_t pat[MAX_PATH]; WIN32_FIND_DATAW fd; DWORD a = GetFileAttributesW(path); if (a == INVALID_FILE_ATTRIBUTES) return; if (!(a & FILE_ATTRIBUTE_DIRECTORY)) { SetFileAttributesW(path, FILE_ATTRIBUTE_NORMAL); DeleteFileW(path); return; } swprintf(pat, MAX_PATH, L"%ls\\*", path); HANDLE h = FindFirstFileW(pat, &fd); if (h != INVALID_HANDLE_VALUE) { do { if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..")) continue; wchar_t c[MAX_PATH]; path_join(c, path, fd.cFileName); rm_tree(c); } while (FindNextFileW(h, &fd)); FindClose(h); } RemoveDirectoryW(path); } /* ---- fetching: https (WinHTTP, redirects followed) or a local folder ---- */ /* to a file (path) or to memory (*mem, *memn); progress from f0 to f1 of the bar */ static int fetch(const wchar_t *url, const wchar_t *path, char **mem, size_t *memn, double f0, double f1, const wchar_t *what) { HANDLE out = INVALID_HANDLE_VALUE; size_t cap = 0, got = 0; char *buf = 0; if (path) { out = CreateFileW(path, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); if (out == INVALID_HANDLE_VALUE) return bld_fail(L"Could not write %ls.", path); } int ok = 0; if (_wcsnicmp(url, L"http", 4)) { /* a folder */ HANDLE in = CreateFileW(url, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); if (in == INVALID_HANDLE_VALUE) { bld_fail(L"%ls is not there.", url); goto end; } LARGE_INTEGER sz; GetFileSizeEx(in, &sz); static char chunk[1 << 20]; DWORD n; while (ReadFile(in, chunk, sizeof chunk, &n, 0) && n) { if (bld.cancel) { CloseHandle(in); goto end; } if (path) { DWORD w; WriteFile(out, chunk, n, &w, 0); } else { if (got + n + 1 > cap) { cap = (got + n + 1) * 2; buf = realloc(buf, cap); } memcpy(buf + got, chunk, n); } got += n; if (sz.QuadPart) bld_status(f0 + (f1 - f0) * (double)got / (double)sz.QuadPart, L"%ls (%.0f of %.0f MB)", what, got / 1048576.0, sz.QuadPart / 1048576.0); } CloseHandle(in); ok = 1; goto end; } { URL_COMPONENTSW uc; wchar_t host[256], upath[2048]; memset(&uc, 0, sizeof uc); uc.dwStructSize = sizeof uc; uc.lpszHostName = host; uc.dwHostNameLength = 256; uc.lpszUrlPath = upath; uc.dwUrlPathLength = 2048; if (!WinHttpCrackUrl(url, 0, 0, &uc)) { bld_fail(L"Bad address: %ls", url); goto end; } HINTERNET s = WinHttpOpen(L"ProjectSolana/1.0", 4 /* WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY (8.1+) */, 0, 0, 0); if (!s) s = WinHttpOpen(L"ProjectSolana/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, 0, 0, 0); HINTERNET c = s ? WinHttpConnect(s, host, uc.nPort, 0) : 0; HINTERNET r = c ? WinHttpOpenRequest(c, L"GET", upath, 0, 0, 0, uc.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0) : 0; DWORD status = 0, sl = sizeof status; unsigned long long total = 0; if (!r || !WinHttpSendRequest(r, 0, 0, 0, 0, 0, 0) || !WinHttpReceiveResponse(r, 0)) { bld_fail(L"Could not download %ls (error %lu). Check your internet connection.", what, GetLastError()); } else { WinHttpQueryHeaders(r, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, 0, &status, &sl, 0); wchar_t lenb[32]; DWORD ll = sizeof lenb; if (WinHttpQueryHeaders(r, WINHTTP_QUERY_CONTENT_LENGTH, 0, lenb, &ll, 0)) total = _wcstoui64(lenb, 0, 10); if (status != 200) bld_fail(L"Could not download %ls (the server answered %lu).", what, status); else { static char chunk[1 << 16]; for (;;) { DWORD n = 0; if (bld.cancel) break; if (!WinHttpReadData(r, chunk, sizeof chunk, &n)) { bld_fail(L"The download of %ls stopped (error %lu).", what, GetLastError()); break; } if (!n) { ok = 1; break; } if (path) { DWORD w; if (!WriteFile(out, chunk, n, &w, 0) || w != n) { bld_fail(L"Could not write %ls (disk full?).", path); break; } } else { if (got + n + 1 > cap) { cap = (got + n + 1) * 2; buf = realloc(buf, cap); } memcpy(buf + got, chunk, n); } got += n; if (total) bld_status(f0 + (f1 - f0) * (double)got / (double)total, L"%ls (%.0f of %.0f MB)", what, got / 1048576.0, total / 1048576.0); else bld_status(-1, L"%ls (%.0f MB)", what, got / 1048576.0); } } } if (r) WinHttpCloseHandle(r); if (c) WinHttpCloseHandle(c); if (s) WinHttpCloseHandle(s); } end: if (out != INVALID_HANDLE_VALUE) CloseHandle(out); if (!ok || bld.cancel) { free(buf); if (path) DeleteFileW(path); return 0; } if (mem) { if (!buf) buf = malloc(1); buf[got] = 0; *mem = buf; *memn = got; } else free(buf); return 1; } /* ---- SHA-256 of a file (CNG) ---- */ static int sha256_file(const wchar_t *path, char hex[65]) { BCRYPT_ALG_HANDLE alg; BCRYPT_HASH_HANDLE h; unsigned char d[32]; int ok = 0; if (BCryptOpenAlgorithmProvider(&alg, BCRYPT_SHA256_ALGORITHM, 0, 0)) return 0; if (!BCryptCreateHash(alg, &h, 0, 0, 0, 0, 0)) { HANDLE f = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0); if (f != INVALID_HANDLE_VALUE) { static unsigned char b[1 << 20]; DWORD n; while (ReadFile(f, b, sizeof b, &n, 0) && n) BCryptHashData(h, b, n, 0); CloseHandle(f); if (!BCryptFinishHash(h, d, 32, 0)) { for (int i = 0; i < 32; i++) sprintf(hex + 2 * i, "%02x", d[i]); hex[64] = 0; ok = 1; } } BCryptDestroyHash(h); } BCryptCloseAlgorithmProvider(alg, 0); return ok; } /* ---- unzip: stored and deflated entries (RFC 1951 inflate) ---- */ typedef struct { const unsigned char *in; size_t inn, ip; unsigned bb; int bc; unsigned char *out; size_t outn, op; int err; } inf_t; typedef struct { short count[16], symbol[320]; } huff_t; static int ibits(inf_t *s, int n) { unsigned v = s->bb; while (s->bc < n) { if (s->ip >= s->inn) { s->err = 1; return 0; } v |= (unsigned)s->in[s->ip++] << s->bc; s->bc += 8; } s->bb = v >> n; s->bc -= n; return (int)(v & ((1u << n) - 1)); } static int hdecode(inf_t *s, const huff_t *h) { int code = 0, first = 0, index = 0; for (int len = 1; len < 16; len++) { code |= ibits(s, 1); int count = h->count[len]; if (code - count < first) return h->symbol[index + (code - first)]; index += count; first += count; first <<= 1; code <<= 1; if (s->err) return -1; } s->err = 1; return -1; } static int hbuild(huff_t *h, const short *len, int n) { short offs[16]; memset(h->count, 0, sizeof h->count); for (int i = 0; i < n; i++) h->count[len[i]]++; if (h->count[0] == n) return 0; int left = 1; for (int l = 1; l < 16; l++) { left <<= 1; left -= h->count[l]; if (left < 0) return left; } offs[1] = 0; for (int l = 1; l < 15; l++) offs[l + 1] = (short)(offs[l] + h->count[l]); for (int i = 0; i < n; i++) if (len[i]) h->symbol[offs[len[i]]++] = (short)i; return left; } static int inflate_codes(inf_t *s, const huff_t *lc, const huff_t *dc) { static const short lbase[29] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258 }; static const short lext[29] = { 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0 }; static const short dbase[30] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577 }; static const short dext[30] = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13 }; for (;;) { int sym = hdecode(s, lc); if (s->err || sym < 0) return 0; if (sym < 256) { if (s->op >= s->outn) return 0; s->out[s->op++] = (unsigned char)sym; } else if (sym == 256) return 1; else { sym -= 257; if (sym >= 29) return 0; int len = lbase[sym] + ibits(s, lext[sym]); int ds = hdecode(s, dc); if (s->err || ds < 0 || ds >= 30) return 0; size_t dist = (size_t)(dbase[ds] + ibits(s, dext[ds])); if (dist > s->op || s->op + (size_t)len > s->outn) return 0; for (int i = 0; i < len; i++, s->op++) s->out[s->op] = s->out[s->op - dist]; } } } static int inflate_mem(const unsigned char *in, size_t inn, unsigned char *out, size_t outn) { inf_t s; memset(&s, 0, sizeof s); s.in = in; s.inn = inn; s.out = out; s.outn = outn; static huff_t lc, dc; int last; do { last = ibits(&s, 1); int type = ibits(&s, 2); if (s.err) return 0; if (type == 0) { s.bb = 0; s.bc = 0; if (s.ip + 4 > s.inn) return 0; unsigned len = s.in[s.ip] | (s.in[s.ip + 1] << 8); s.ip += 4; if (s.ip + len > s.inn || s.op + len > s.outn) return 0; memcpy(s.out + s.op, s.in + s.ip, len); s.ip += len; s.op += len; } else if (type == 1) { static int fixed_done; static huff_t flc, fdc; if (!fixed_done) { short l[288]; int i; for (i = 0; i < 144; i++) l[i] = 8; for (; i < 256; i++) l[i] = 9; for (; i < 280; i++) l[i] = 7; for (; i < 288; i++) l[i] = 8; hbuild(&flc, l, 288); for (i = 0; i < 30; i++) l[i] = 5; hbuild(&fdc, l, 30); fixed_done = 1; } if (!inflate_codes(&s, &flc, &fdc)) return 0; } else if (type == 2) { static const unsigned char ord[19] = { 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 }; short l[320]; int nlen = ibits(&s, 5) + 257, ndist = ibits(&s, 5) + 1, ncode = ibits(&s, 4) + 4, i; if (s.err || nlen > 286 || ndist > 30) return 0; for (i = 0; i < ncode; i++) l[ord[i]] = (short)ibits(&s, 3); for (; i < 19; i++) l[ord[i]] = 0; if (hbuild(&lc, l, 19)) return 0; for (i = 0; i < nlen + ndist;) { int sym = hdecode(&s, &lc); if (s.err || sym < 0) return 0; if (sym < 16) l[i++] = (short)sym; else { int rep, v = 0; if (sym == 16) { if (!i) return 0; v = l[i - 1]; rep = 3 + ibits(&s, 2); } else if (sym == 17) rep = 3 + ibits(&s, 3); else rep = 11 + ibits(&s, 7); if (i + rep > nlen + ndist) return 0; while (rep--) l[i++] = (short)v; } } if (l[256] == 0) return 0; if (hbuild(&lc, l, nlen) < 0 && nlen - lc.count[0] != 1) return 0; if (hbuild(&dc, l + nlen, ndist) < 0 && ndist - dc.count[0] != 1) return 0; if (!inflate_codes(&s, &lc, &dc)) return 0; } else return 0; } while (!last); return s.op == s.outn; } static unsigned rd16(const unsigned char *p) { return p[0] | (p[1] << 8); } static unsigned rd32(const unsigned char *p) { return p[0] | (p[1] << 8) | (p[2] << 16) | ((unsigned)p[3] << 24); } /* every entry of the zip into dir (which it creates); progress f0..f1 */ static int unzip(const wchar_t *zip, const wchar_t *dir, double f0, double f1, const wchar_t *what) { HANDLE f = CreateFileW(zip, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); if (f == INVALID_HANDLE_VALUE) return bld_fail(L"Could not open %ls.", zip); LARGE_INTEGER sz; GetFileSizeEx(f, &sz); size_t n = (size_t)sz.QuadPart; unsigned char *z = malloc(n ? n : 1); DWORD got = 0; size_t at = 0; while (z && at < n && ReadFile(f, z + at, (DWORD)((n - at) > (1u << 30) ? (1u << 30) : (n - at)), &got, 0) && got) at += got; CloseHandle(f); if (!z || at != n) { free(z); return bld_fail(L"Could not read %ls.", zip); } size_t e = n >= 22 ? n - 22 : 0, stop = n > 65557 ? n - 65557 : 0; while (e > stop && rd32(z + e) != 0x06054b50) e--; if (rd32(z + e) != 0x06054b50) { free(z); return bld_fail(L"%ls is not a zip file.", what); } unsigned cnt = rd16(z + e + 10); size_t cd = rd32(z + e + 16); mkdirs(dir); int ok = 1; for (unsigned i = 0; i < cnt && ok; i++) { if (bld.cancel) { ok = 0; break; } if (cd + 46 > n || rd32(z + cd) != 0x02014b50) { ok = bld_fail(L"%ls is damaged.", what); break; } unsigned method = rd16(z + cd + 10), nl = rd16(z + cd + 28), xl = rd16(z + cd + 30), cl = rd16(z + cd + 32); WORD dtime = (WORD)rd16(z + cd + 12), ddate = (WORD)rd16(z + cd + 14); /* (kept: a retry of the same kit reuses the compiled files) */ size_t csize = rd32(z + cd + 20), usize = rd32(z + cd + 24), lo = rd32(z + cd + 42); char name[512]; if (nl >= sizeof name) { ok = bld_fail(L"%ls has a name too long.", what); break; } memcpy(name, z + cd + 46, nl); name[nl] = 0; cd += 46 + nl + xl + cl; if (strstr(name, "..") || name[0] == '/' || name[0] == '\\' || strchr(name, ':')) continue; /* (never outside dir) */ wchar_t wn[512], p[MAX_PATH]; MultiByteToWideChar(CP_UTF8, 0, name, -1, wn, 512); for (wchar_t *c = wn; *c; c++) if (*c == L'/') *c = L'\\'; path_join(p, dir, wn); size_t wl = wcslen(p); if (wl && p[wl - 1] == L'\\') { p[wl - 1] = 0; mkdirs(p); continue; } { wchar_t d2[MAX_PATH]; wcscpy(d2, p); wchar_t *s2 = wcsrchr(d2, L'\\'); if (s2) { *s2 = 0; mkdirs(d2); } } if (lo + 30 > n || rd32(z + lo) != 0x04034b50) { ok = bld_fail(L"%ls is damaged.", what); break; } size_t data = lo + 30 + rd16(z + lo + 26) + rd16(z + lo + 28); if (data + csize > n) { ok = bld_fail(L"%ls is damaged.", what); break; } unsigned char *u = method == 0 ? z + data : malloc(usize ? usize : 1); if (method == 8 && (!u || !inflate_mem(z + data, csize, u, usize))) { free(u); ok = bld_fail(L"%ls is damaged (%hs).", what, name); break; } if (method != 0 && method != 8) { ok = bld_fail(L"%ls uses an unknown compression.", what); break; } HANDLE o = CreateFileW(p, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); if (o == INVALID_HANDLE_VALUE) { if (method == 8) free(u); ok = bld_fail(L"Could not write %ls.", p); break; } size_t w0 = 0; DWORD w; while (w0 < usize && WriteFile(o, u + w0, (DWORD)((usize - w0) > (1u << 30) ? (1u << 30) : (usize - w0)), &w, 0) && w) w0 += w; { FILETIME lt, ft; if (DosDateTimeToFileTime(ddate, dtime, <) && LocalFileTimeToFileTime(<, &ft)) SetFileTime(o, 0, 0, &ft); } CloseHandle(o); if (method == 8) free(u); if (w0 != usize) { ok = bld_fail(L"Could not write %ls (disk full?).", p); break; } if (i % 16 == 0) bld_status(f0 + (f1 - f0) * (double)(i + 1) / cnt, L"Unpacking %ls (%u of %u files)", what, i + 1, cnt); } free(z); return ok && !bld.cancel; } /* ---- the release's description: a flat JSON object ---- */ static int json_get(const char *js, const char *key, char *out, size_t n) { char k[96]; snprintf(k, sizeof k, "\"%s\"", key); const char *p = strstr(js, k); if (!p) return 0; p += strlen(k); while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n' || *p == ':') p++; size_t i = 0; if (*p == '"') { p++; while (*p && *p != '"' && i + 1 < n) out[i++] = *p++; } else while (*p && *p != ',' && *p != '}' && *p != ' ' && *p != '\r' && *p != '\n' && i + 1 < n) out[i++] = *p++; out[i] = 0; return 1; } /* ---- running build.py ---- */ static void build_line(const char *l, double f0, double f1) { char ph[32]; int a, b; if (sscanf(l, "[progress] %31s %d %d", ph, &a, &b) != 3 || b <= 0) return; double x = (double)a / b, lo, hi; /* where each phase sits between f0 and f1 */ const wchar_t *what; if (!strcmp(ph, "prepare")) { lo = 0.00; hi = 0.08; what = L"Translating the game's executable"; } else if (!strcmp(ph, "levels")) { lo = 0.08; hi = 0.30; what = L"Translating the levels"; } else if (!strcmp(ph, "compile")) { lo = 0.30; hi = 0.97; what = L"Compiling"; } else if (!strcmp(ph, "link")) { lo = 0.97; hi = 0.99; what = L"Linking"; } else { lo = 1; hi = 1; what = L"Finishing"; } double f = f0 + (f1 - f0) * (lo + (hi - lo) * x); if (!strcmp(ph, "levels")) bld_status(f, L"%ls (%d of %d)", what, a + 1, b); else if (!strcmp(ph, "compile")) bld_status(f, L"%ls (%d of %d files)", what, a, b); else bld_status(f, L"%ls", what); } static int run_build(const wchar_t *gdir, const wchar_t *iso, const wchar_t *levels, const wchar_t *rgn, double f0, double f1) { wchar_t py[MAX_PATH], zig[MAX_PATH], tc[MAX_PATH], cmd[4096], logp[MAX_PATH]; path_join(tc, base_dir, L"toolchain"); swprintf(py, MAX_PATH, L"%ls\\python\\python.exe", tc); swprintf(zig, MAX_PATH, L"%ls\\zig", tc); /* jobs: the cores, but no more than the memory allows (a big generated file can take 1.5 GB to compile) */ SYSTEM_INFO si; GetSystemInfo(&si); MEMORYSTATUSEX ms; ms.dwLength = sizeof ms; GlobalMemoryStatusEx(&ms); int j = (int)si.dwNumberOfProcessors, jm = (int)(ms.ullTotalPhys / (1536ull << 20)); if (j > jm) j = jm; if (j < 1) j = 1; swprintf(cmd, 4096, L"\"%ls\" -u build.py --platform windows --region %ls --levels %ls --iso \"%ls\" --repo decomp -j %d", py, rgn, levels, iso, j); static wchar_t path[8192]; static int have_path; /* (the PATH this program started with) */ if (!have_path) { DWORD k = GetEnvironmentVariableW(L"PATH", path, 8192); if (k >= 8192) path[0] = 0; have_path = 1; } { /* the toolchain first on PATH; Python in UTF-8 mode (the sources are UTF-8) */ wchar_t np[9000]; swprintf(np, 9000, L"%ls;%ls\\python;%ls", zig, tc, path); SetEnvironmentVariableW(L"PATH", np); SetEnvironmentVariableW(L"PYTHONUTF8", L"1"); SetEnvironmentVariableW(L"PYTHONDONTWRITEBYTECODE", L"1"); SetEnvironmentVariableW(L"PYTHONHOME", 0); SetEnvironmentVariableW(L"PYTHONPATH", 0); swprintf(np, 9000, L"%ls\\zig-cache", gdir); SetEnvironmentVariableW(L"ZIG_GLOBAL_CACHE_DIR", np); SetEnvironmentVariableW(L"ZIG_LOCAL_CACHE_DIR", np); } path_join(logp, gdir, L"build_log.txt"); HANDLE lf = CreateFileW(logp, GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0); SECURITY_ATTRIBUTES sa = { sizeof sa, 0, TRUE }; HANDLE rd, wr; if (!CreatePipe(&rd, &wr, &sa, 0)) { if (lf != INVALID_HANDLE_VALUE) CloseHandle(lf); return bld_fail(L"Could not start the build (pipe)."); } SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0); STARTUPINFOW st; PROCESS_INFORMATION pi; memset(&st, 0, sizeof st); st.cb = sizeof st; st.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; st.wShowWindow = SW_HIDE; st.hStdOutput = wr; st.hStdError = wr; st.hStdInput = GetStdHandle(STD_INPUT_HANDLE); bld.job = CreateJobObjectW(0, 0); if (bld.job) { JOBOBJECT_EXTENDED_LIMIT_INFORMATION li; memset(&li, 0, sizeof li); li.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; SetInformationJobObject(bld.job, JobObjectExtendedLimitInformation, &li, sizeof li); } if (!CreateProcessW(py, cmd, 0, 0, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, 0, gdir, &st, &pi)) { CloseHandle(rd); CloseHandle(wr); if (lf != INVALID_HANDLE_VALUE) CloseHandle(lf); return bld_fail(L"Could not start the build tools (error %lu).", GetLastError()); } if (bld.job) AssignProcessToJobObject(bld.job, pi.hProcess); ResumeThread(pi.hThread); CloseHandle(pi.hThread); CloseHandle(wr); static char tail[16][300]; int nt = 0; char line[4096]; size_t ln = 0; char b[4096]; DWORD n; while (ReadFile(rd, b, sizeof b, &n, 0) && n) { DWORD w; if (lf != INVALID_HANDLE_VALUE) WriteFile(lf, b, n, &w, 0); for (DWORD i = 0; i < n; i++) { if (b[i] == '\n' || ln + 1 >= sizeof line) { line[ln] = 0; if (ln && line[ln - 1] == '\r') line[ln - 1] = 0; if (!strncmp(line, "[progress]", 10)) build_line(line, f0, f1); else if (line[0]) { snprintf(tail[nt % 16], 300, "%s", line); nt++; } ln = 0; } else line[ln++] = b[i]; } if (bld.cancel) TerminateJobObject(bld.job, 1); } CloseHandle(rd); WaitForSingleObject(pi.hProcess, INFINITE); DWORD code = 1; GetExitCodeProcess(pi.hProcess, &code); CloseHandle(pi.hProcess); if (bld.job) { CloseHandle(bld.job); bld.job = 0; } if (lf != INVALID_HANDLE_VALUE) CloseHandle(lf); if (bld.cancel) return 0; if (code) { const char *last = nt ? tail[(nt - 1) % 16] : "no output"; return bld_fail(L"The build stopped (code %lu): %hs", code, last); } return 1; } /* ---- the Build button's thread ---- */ static DWORD WINAPI build_thread(LPVOID unused) { (void)unused; disc_t *d = &discs[bld.disc]; const game_t *g = &games[d->game]; wchar_t rel[1024], url[1200], dl[MAX_PATH], tcdir[MAX_PATH], gdir[MAX_PATH], zp[MAX_PATH], p[MAX_PATH]; char *js = 0; size_t jn = 0; GetPrivateProfileStringW(L"build", L"release_url", RELEASE_URL, rel, 1024, ini_path); int local = _wcsnicmp(rel, L"http", 4) != 0; const wchar_t *sep = local ? L"\\" : L"/"; size_t rl = wcslen(rel); if (rl && (rel[rl - 1] == L'/' || rel[rl - 1] == L'\\')) rel[--rl] = 0; path_join(dl, base_dir, L"downloads"); mkdirs(dl); path_join(tcdir, base_dir, L"toolchain"); swprintf(gdir, MAX_PATH, L"%ls\\games\\%ls", base_dir, g->kit); /* 1. what to fetch */ bld_status(0.0, L"Checking for the latest build files"); swprintf(url, 1200, L"%ls%ls%ls%ls%ls.json", rel, sep, g->kit, sep, g->kit); if (!fetch(url, 0, &js, &jn, 0.0, 0.01, L"the build description")) goto out; char version[64] = "", tcv[64] = "", tcf[128] = "", tcs[80] = "", kitf[128] = "", kits[80] = "", lv[256] = ""; json_get(js, "version", version, sizeof version); json_get(js, "toolchain_version", tcv, sizeof tcv); json_get(js, "toolchain_file", tcf, sizeof tcf); json_get(js, "toolchain_sha256", tcs, sizeof tcs); json_get(js, "kit_file", kitf, sizeof kitf); json_get(js, "kit_sha256", kits, sizeof kits); json_get(js, "levels", lv, sizeof lv); if (!version[0] || !tcf[0] || !kitf[0]) { bld_fail(L"The build description from the release is not valid."); goto out; } /* 2. the toolchain, when it is missing or another version */ { wchar_t have[64] = L"", want[64], vf[MAX_PATH]; MultiByteToWideChar(CP_UTF8, 0, tcv, -1, want, 64); path_join(vf, tcdir, L"version.txt"); { FILE *f = _wfopen(vf, L"r"); if (f) { char b[64] = ""; if (fgets(b, sizeof b, f)) { b[strcspn(b, "\r\n")] = 0; MultiByteToWideChar(CP_UTF8, 0, b, -1, have, 64); } fclose(f); } } swprintf(p, MAX_PATH, L"%ls\\python\\python.exe", tcdir); if (wcscmp(have, want) || GetFileAttributesW(p) == INVALID_FILE_ATTRIBUTES) { wchar_t wf[128]; MultiByteToWideChar(CP_UTF8, 0, tcf, -1, wf, 128); swprintf(url, 1200, L"%ls%ls%ls%ls%ls", rel, sep, g->kit, sep, wf); path_join(zp, dl, wf); if (!fetch(url, zp, 0, 0, 0.01, 0.20, L"Downloading the build tools")) goto out; bld_status(0.20, L"Checking the build tools"); char h[65]; if (tcs[0] && (!sha256_file(zp, h) || _stricmp(h, tcs))) { DeleteFileW(zp); bld_fail(L"The downloaded build tools are damaged (checksum). Try again."); goto out; } rm_tree(tcdir); if (!unzip(zp, tcdir, 0.20, 0.26, L"the build tools")) goto out; FILE *f = _wfopen(vf, L"w"); if (f) { fputs(tcv, f); fclose(f); } DeleteFileW(zp); } } /* 3. the game's build kit (a fresh copy; the game's own folder, build\, stays: saves live there) */ { wchar_t wf[128]; MultiByteToWideChar(CP_UTF8, 0, kitf, -1, wf, 128); swprintf(url, 1200, L"%ls%ls%ls%ls%ls", rel, sep, g->kit, sep, wf); path_join(zp, dl, wf); if (!fetch(url, zp, 0, 0, 0.26, 0.30, L"Downloading the game's build kit")) goto out; char h[65]; if (kits[0] && (!sha256_file(zp, h) || _stricmp(h, kits))) { DeleteFileW(zp); bld_fail(L"The downloaded build kit is damaged (checksum). Try again."); goto out; } mkdirs(gdir); { /* another kit version than the files compiled last time: none of them can be reused */ wchar_t kv[64] = L"", wv[64]; GetPrivateProfileStringW(g->kit, L"kit_version", L"", kv, 64, ini_path); MultiByteToWideChar(CP_UTF8, 0, version, -1, wv, 64); if (kv[0] && wcscmp(kv, wv)) { /* (none recorded: a first build, or one from before this was kept) */ static const wchar_t *old[] = { L"build\\windows\\gen", L"build\\windows\\gen.new", L"build\\windows\\obj", L"build\\windows_us\\gen", L"build\\windows_us\\gen.new", L"build\\windows_us\\obj", L"zig-cache" }; for (unsigned i = 0; i < sizeof old / sizeof old[0]; i++) { path_join(p, gdir, old[i]); rm_tree(p); } } WritePrivateProfileStringW(g->kit, L"kit_version", wv, ini_path); } { /* everything but build\ */ wchar_t pat[MAX_PATH]; WIN32_FIND_DATAW fd; swprintf(pat, MAX_PATH, L"%ls\\*", gdir); HANDLE hf = FindFirstFileW(pat, &fd); if (hf != INVALID_HANDLE_VALUE) { do { if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..") || !_wcsicmp(fd.cFileName, L"build")) continue; path_join(p, gdir, fd.cFileName); rm_tree(p); } while (FindNextFileW(hf, &fd)); FindClose(hf); } } if (!unzip(zp, gdir, 0.30, 0.32, L"the build kit")) goto out; DeleteFileW(zp); } /* 4. build */ { wchar_t levels[256]; GetPrivateProfileStringW(L"build", L"levels", L"", levels, 256, ini_path); if (!levels[0]) MultiByteToWideChar(CP_UTF8, 0, lv[0] ? lv : "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18", -1, levels, 256); bld_status(0.32, L"Starting the build"); if (!run_build(gdir, d->path, levels, g->rgn ? g->rgn : L"pal", 0.32, 0.99)) goto out; } /* 5. keep what the game needs: the intermediate files (about 1.3 GB) and the bytes read from the disc go */ bld_status(0.99, L"Cleaning up"); { static const wchar_t *junk[] = { L"gen", L"gen.new", L"obj", L"region", L"rac1_game.pdb", L"rac1_wrapper.pdb", L"asm_stubs.c.lib" }; /* (in the build's own folder) */ static const wchar_t *junk2[] = { L"build\\levels", L"build\\levels_us", L"build\\disc", L"zig-cache" }; const wchar_t *bd = g->rgn && wcscmp(g->rgn, L"pal") ? L"build\\windows_" : L"build\\windows"; for (unsigned i = 0; i < sizeof junk / sizeof junk[0]; i++) { swprintf(p, MAX_PATH, L"%ls\\%ls%ls\\%ls", gdir, bd, g->rgn && wcscmp(g->rgn, L"pal") ? g->rgn : L"", junk[i]); rm_tree(p); } for (unsigned i = 0; i < sizeof junk2 / sizeof junk2[0]; i++) { path_join(p, gdir, junk2[i]); rm_tree(p); } } { wchar_t wv[64]; MultiByteToWideChar(CP_UTF8, 0, version, -1, wv, 64); WritePrivateProfileStringW(g->kit, g->rgn ? L"built_version_us" : L"built_version", wv, ini_path); } bld_status(1.0, L"Built"); out: free(js); if (bld.cancel && !bld.failed) bld_fail(L"Build cancelled."); bld.active = 0; PostMessageW(hwnd, WM_BUILD_UPDATE, 1, 0); return 0; } static void build_start(int i) { if (bld.active || i < 0 || discs[i].game < 0 || !games[discs[i].game].kit) return; bld.active = 1; bld.cancel = 0; bld.failed = 0; bld.disc = i; bld.frac = 0; bld.err[0] = 0; wcscpy(bld.status, L"Starting"); message[0] = 0; CloseHandle(CreateThread(0, 0, build_thread, 0, 0, 0)); }