553 lines
30 KiB
C
553 lines
30 KiB
C
/* 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:
|
|
* <tag>.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
|
|
* <tag>-build.zip the game's build kit: build.py, the recompilers, the
|
|
* host engine and the decomp's C, unpacked to games\<tag>\
|
|
* then runs build.py on the disc image and keeps only what the game needs
|
|
* (games\<tag>\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=<url or folder> and levels=<list> override the
|
|
* source and the levels (testing, offline installs). */
|
|
#include <winhttp.h>
|
|
#include <bcrypt.h>
|
|
|
|
#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));
|
|
}
|