#ifndef NOMINMAX #define NOMINMAX #endif #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include #include #include #include #include #include #define MAX_PATH_LEN 1024 #define MAX_FILES 4096 #define MAX_HASH_ENTRIES 512 // ============================================================================ // Arena Allocator // ============================================================================ typedef struct { char* base; size_t size; size_t offset; } Arena; static Arena g_Arena; static void ArenaInit(size_t size) { g_Arena.base = (char*)malloc(size); assert(g_Arena.base != NULL); g_Arena.size = size; g_Arena.offset = 0; } static void* ArenaPush(size_t size) { // align to 8 bytes size_t aligned_size = (size + 7) & ~7; if (g_Arena.offset + aligned_size > g_Arena.size) { printf("[ERROR] Arena out of memory!\n"); exit(1); } void* ptr = g_Arena.base + g_Arena.offset; g_Arena.offset += aligned_size; memset(ptr, 0, size); return ptr; } // ============================================================================ // String helpers // ============================================================================ static bool StringContains(const char* haystack, const char* needle) { return strstr(haystack, needle) != NULL; } static bool EndsWithNoCase(const char* str, const char* suffix) { int strLen = (int)strlen(str); int sufLen = (int)strlen(suffix); if (sufLen > strLen) return false; return _stricmp(str + strLen - sufLen, suffix) == 0; } // ============================================================================ // File I/O helpers // ============================================================================ typedef struct { char* Data; long Size; } FileContent; static FileContent ReadEntireFile(const char* path) { FileContent result = { NULL, 0 }; FILE* f = NULL; fopen_s(&f, path, "rb"); if (!f) return result; fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); if (size > 0) { result.Data = (char*)ArenaPush(size + 1); result.Size = size; fread(result.Data, 1, size, f); result.Data[size] = '\0'; } fclose(f); return result; } static bool WriteEntireFile(const char* path, const char* data, long size) { FILE* f = NULL; fopen_s(&f, path, "wb"); if (!f) return false; size_t written = fwrite(data, 1, size, f); fclose(f); return written == (size_t)size; } static bool FileExists(const char* path) { DWORD attribs = GetFileAttributesA(path); return (attribs != INVALID_FILE_ATTRIBUTES) && !(attribs & FILE_ATTRIBUTE_DIRECTORY); } static bool DirectoryExists(const char* path) { DWORD attribs = GetFileAttributesA(path); return (attribs != INVALID_FILE_ATTRIBUTES) && (attribs & FILE_ATTRIBUTE_DIRECTORY); } static void EnsureDirectoryExists(const char* path) { char tmp[MAX_PATH_LEN]; strncpy_s(tmp, MAX_PATH_LEN, path, _TRUNCATE); for (int i = 0; tmp[i]; i++) { if (tmp[i] == '\\' || tmp[i] == '/') { char c = tmp[i]; tmp[i] = '\0'; CreateDirectoryA(tmp, NULL); tmp[i] = c; } } CreateDirectoryA(tmp, NULL); } static int64_t GetFileModTimeInt64(const char* path) { WIN32_FILE_ATTRIBUTE_DATA data; if (GetFileAttributesExA(path, GetFileExInfoStandard, &data)) { ULARGE_INTEGER li; li.LowPart = data.ftLastWriteTime.dwLowDateTime; li.HighPart = data.ftLastWriteTime.dwHighDateTime; return (int64_t)li.QuadPart; } return 0; } // ============================================================================ // Recursive directory scan // ============================================================================ static int64_t GetNewestSourceTimeRecursive(const char* dirPath) { int64_t newest = 0; if (!DirectoryExists(dirPath)) return newest; char searchPath[MAX_PATH_LEN]; snprintf(searchPath, sizeof(searchPath), "%s\\*", dirPath); WIN32_FIND_DATAA findData; HANDLE hFind = FindFirstFileA(searchPath, &findData); if (hFind == INVALID_HANDLE_VALUE) return newest; do { if (findData.cFileName[0] == '.' && (findData.cFileName[1] == '\0' || (findData.cFileName[1] == '.' && findData.cFileName[2] == '\0'))) { continue; } char fullPath[MAX_PATH_LEN]; snprintf(fullPath, sizeof(fullPath), "%s\\%s", dirPath, findData.cFileName); if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { int64_t subNewest = GetNewestSourceTimeRecursive(fullPath); if (subNewest > newest) newest = subNewest; } else { if (EndsWithNoCase(findData.cFileName, ".cpp") || EndsWithNoCase(findData.cFileName, ".h") || EndsWithNoCase(findData.cFileName, ".hpp") || EndsWithNoCase(findData.cFileName, ".inl") || EndsWithNoCase(findData.cFileName, ".c")) { ULARGE_INTEGER li; li.LowPart = findData.ftLastWriteTime.dwLowDateTime; li.HighPart = findData.ftLastWriteTime.dwHighDateTime; int64_t ft = (int64_t)li.QuadPart; if (ft > newest) newest = ft; } } } while (FindNextFileA(hFind, &findData)); FindClose(hFind); return newest; } // ============================================================================ // Collect unity files // ============================================================================ typedef struct { char (*Paths)[MAX_PATH_LEN]; int Count; } FilePathList; static FilePathList* CreateFilePathList() { FilePathList* list = (FilePathList*)ArenaPush(sizeof(FilePathList)); list->Paths = (char(*)[MAX_PATH_LEN])ArenaPush(MAX_FILES * MAX_PATH_LEN); list->Count = 0; return list; } static void FilePathListAdd(FilePathList* list, const char* path) { if (list->Count < MAX_FILES) { strncpy_s(list->Paths[list->Count], MAX_PATH_LEN, path, _TRUNCATE); list->Count++; } } static void CollectFiles(const char* dirPath, const char* containsStr, const char* endsWithStr, FilePathList* list) { char searchPath[MAX_PATH_LEN]; snprintf(searchPath, sizeof(searchPath), "%s\\*", dirPath); WIN32_FIND_DATAA findData; HANDLE hFind = FindFirstFileA(searchPath, &findData); if (hFind == INVALID_HANDLE_VALUE) return; do { if (findData.cFileName[0] == '.' && (findData.cFileName[1] == '\0' || (findData.cFileName[1] == '.' && findData.cFileName[2] == '\0'))) { continue; } char fullPath[MAX_PATH_LEN]; snprintf(fullPath, sizeof(fullPath), "%s\\%s", dirPath, findData.cFileName); if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { CollectFiles(fullPath, containsStr, endsWithStr, list); } else { bool match = true; if (containsStr && !StringContains(findData.cFileName, containsStr)) match = false; if (endsWithStr && !EndsWithNoCase(findData.cFileName, endsWithStr)) match = false; if (match) { FilePathListAdd(list, fullPath); } } } while (FindNextFileA(hFind, &findData)); FindClose(hFind); } typedef struct { char SourceDir[MAX_PATH_LEN]; char OutputDir[MAX_PATH_LEN]; char Prefix[256]; char* ExplicitFiles[64]; int ExplicitFileCount; int MaxFiles; } UnityConfig; static bool GenerateUnityFiles(const UnityConfig* ucfg) { FilePathList* srcFiles = CreateFilePathList(); if (ucfg->SourceDir[0] != '\0') { CollectFiles(ucfg->SourceDir, NULL, ".cpp", srcFiles); } else { for (int i = 0; i < ucfg->ExplicitFileCount; i++) { FilePathListAdd(srcFiles, ucfg->ExplicitFiles[i]); } } EnsureDirectoryExists(ucfg->OutputDir); int fileIdx = 0; int unityCount = 1; char currentUnityPath[MAX_PATH_LEN]; bool anyChanged = false; char* memBuf = (char*)ArenaPush(1024 * 1024); int memOffset = 0; for (int i = 0; i < srcFiles->Count; i++) { if (StringContains(srcFiles->Paths[i], "_Unity")) continue; if (fileIdx == 0) { memOffset = 0; } char absPath[MAX_PATH_LEN]; GetFullPathNameA(srcFiles->Paths[i], MAX_PATH_LEN, absPath, NULL); for (int k = 0; absPath[k]; k++) { if (absPath[k] == '\\') absPath[k] = '/'; } memOffset += sprintf_s(memBuf + memOffset, (1024 * 1024) - memOffset, "#include \"%s\"\n", absPath); fileIdx++; if (fileIdx >= ucfg->MaxFiles || i == srcFiles->Count - 1) { snprintf(currentUnityPath, sizeof(currentUnityPath), "%s\\%s%d.cpp", ucfg->OutputDir, ucfg->Prefix, unityCount); FileContent exist = ReadEntireFile(currentUnityPath); bool write = true; if (exist.Data && exist.Size == memOffset) { if (memcmp(exist.Data, memBuf, memOffset) == 0) { write = false; } } if (write) { WriteEntireFile(currentUnityPath, memBuf, memOffset); anyChanged = true; } unityCount++; fileIdx = 0; } } while (true) { snprintf(currentUnityPath, sizeof(currentUnityPath), "%s\\%s%d.cpp", ucfg->OutputDir, ucfg->Prefix, unityCount); if (FileExists(currentUnityPath)) { DeleteFileA(currentUnityPath); anyChanged = true; unityCount++; } else { break; } } return anyChanged; } // ============================================================================ // Commands // ============================================================================ typedef struct { char StepName[256]; char OutputFile[MAX_PATH_LEN]; char Depends[MAX_PATH_LEN]; char Command[MAX_FILES]; } BuildStep; typedef struct { char Name[64]; UnityConfig UnitySteps[16]; int UnityStepCount; BuildStep Steps[32]; int StepCount; } BuildConfig; static int ExecuteCmd(const char* cmdLine) { STARTUPINFOA si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); PROCESS_INFORMATION pi = { 0 }; DWORD exitCode = 1; char* cmdBuf = (char*)ArenaPush(MAX_FILES); strcpy_s(cmdBuf, MAX_FILES, cmdLine); if (CreateProcessA(NULL, cmdBuf, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) { WaitForSingleObject(pi.hProcess, INFINITE); GetExitCodeProcess(pi.hProcess, &exitCode); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } else { printf("[ERROR] Failed to execute command.\n"); return 1; } return (int)exitCode; } typedef struct { char Path[MAX_PATH_LEN]; int64_t Time; } DirCacheEntry; static DirCacheEntry g_DirCache[64]; static int g_DirCacheCount = 0; static int64_t GetDirTime(const char* dir) { for (int k = 0; k < g_DirCacheCount; k++) { if (_stricmp(g_DirCache[k].Path, dir) == 0) return g_DirCache[k].Time; } int64_t t = GetNewestSourceTimeRecursive(dir); if (g_DirCacheCount < 64) { strncpy_s(g_DirCache[g_DirCacheCount].Path, MAX_PATH_LEN, dir, _TRUNCATE); g_DirCache[g_DirCacheCount].Time = t; g_DirCacheCount++; } return t; } static char g_TimingsBuf[MAX_FILES * 2]; static int g_TimingsLen = 0; static void AppendTiming(const char* stepName, double secs) { g_TimingsLen += sprintf_s(g_TimingsBuf + g_TimingsLen, sizeof(g_TimingsBuf) - g_TimingsLen, "%s|%.3fs\n", stepName, secs); } static int CommandRunPlan(int argc, char* argv[]) { if (argc < 3) { printf("[ERROR] Usage: build_system run_plan \n"); return 1; } const char* planFile = argv[2]; FileContent planContent = ReadEntireFile(planFile); if (!planContent.Data) { printf("[ERROR] Could not read build plan: %s\n", planFile); return 1; } EnsureDirectoryExists("Intermediate"); DeleteFileA("Intermediate\\step_timings.tmp"); DeleteFileA("Intermediate\\built_outputs.tmp"); LARGE_INTEGER perfStart, perfEnd, perfFreq; QueryPerformanceCounter(&perfStart); QueryPerformanceFrequency(&perfFreq); // Parse plan file BuildConfig* configs = (BuildConfig*)ArenaPush(4 * sizeof(BuildConfig)); int configCount = 0; const char* pos = planContent.Data; while (*pos != '\0') { const char* lineStart = pos; while (*pos != '\0' && *pos != '\n' && *pos != '\r') pos++; int lineLen = (int)(pos - lineStart); if (lineLen > 0) { if (lineLen > 7 && memcmp(lineStart, "CONFIG ", 7) == 0) { if (configCount < 4) { int nLen = lineLen - 7; if (nLen >= (int)sizeof(configs[configCount].Name)) nLen = (int)sizeof(configs[configCount].Name) - 1; memcpy(configs[configCount].Name, lineStart + 7, nLen); configs[configCount].Name[nLen] = '\0'; configCount++; } } else if (configCount > 0) { BuildConfig* curCfg = &configs[configCount - 1]; if (lineLen > 6 && memcmp(lineStart, "UNITY ", 6) == 0) { if (curCfg->UnityStepCount < 16) { char lineBuf[1024]; int nLen = lineLen - 6; if (nLen >= (int)sizeof(lineBuf)) nLen = (int)sizeof(lineBuf) - 1; memcpy(lineBuf, lineStart + 6, nLen); lineBuf[nLen] = '\0'; UnityConfig* ucfg = &curCfg->UnitySteps[curCfg->UnityStepCount]; ucfg->ExplicitFileCount = 0; if (sscanf_s(lineBuf, "%s %s %s %d", ucfg->SourceDir, (unsigned)_countof(ucfg->SourceDir), ucfg->OutputDir, (unsigned)_countof(ucfg->OutputDir), ucfg->Prefix, (unsigned)_countof(ucfg->Prefix), &ucfg->MaxFiles) == 4) { curCfg->UnityStepCount++; } } } else if (lineLen > 15 && memcmp(lineStart, "UNITY_EXPLICIT ", 15) == 0) { if (curCfg->UnityStepCount < 16) { char lineBuf[2048]; int nLen = lineLen - 15; if (nLen >= (int)sizeof(lineBuf)) nLen = (int)sizeof(lineBuf) - 1; memcpy(lineBuf, lineStart + 15, nLen); lineBuf[nLen] = '\0'; UnityConfig* ucfg = &curCfg->UnitySteps[curCfg->UnityStepCount]; ucfg->SourceDir[0] = '\0'; ucfg->ExplicitFileCount = 0; char fileList[1024]; if (sscanf_s(lineBuf, "%s %s %d %s", ucfg->OutputDir, (unsigned)_countof(ucfg->OutputDir), ucfg->Prefix, (unsigned)_countof(ucfg->Prefix), &ucfg->MaxFiles, fileList, (unsigned)_countof(fileList)) == 4) { char* ctx = NULL; char* token = strtok_s(fileList, "|", &ctx); while(token) { if (ucfg->ExplicitFileCount < 64) { size_t tLen = strlen(token); char* storedStr = (char*)ArenaPush(tLen + 1); strcpy_s(storedStr, tLen + 1, token); ucfg->ExplicitFiles[ucfg->ExplicitFileCount++] = storedStr; } token = strtok_s(NULL, "|", &ctx); } curCfg->UnityStepCount++; } } } else if (lineLen > 5 && memcmp(lineStart, "STEP ", 5) == 0) { if (curCfg->StepCount < 32) { int nLen = lineLen - 5; if (nLen >= (int)sizeof(curCfg->Steps[curCfg->StepCount].StepName)) nLen = (int)sizeof(curCfg->Steps[curCfg->StepCount].StepName) - 1; memcpy(curCfg->Steps[curCfg->StepCount].StepName, lineStart + 5, nLen); curCfg->Steps[curCfg->StepCount].StepName[nLen] = '\0'; curCfg->StepCount++; } } else if (lineLen > 7 && memcmp(lineStart, "OUTPUT ", 7) == 0 && curCfg->StepCount > 0) { int nLen = lineLen - 7; if (nLen >= (int)sizeof(curCfg->Steps[curCfg->StepCount - 1].OutputFile)) nLen = (int)sizeof(curCfg->Steps[curCfg->StepCount - 1].OutputFile) - 1; memcpy(curCfg->Steps[curCfg->StepCount - 1].OutputFile, lineStart + 7, nLen); curCfg->Steps[curCfg->StepCount - 1].OutputFile[nLen] = '\0'; } else if (lineLen > 8 && memcmp(lineStart, "DEPENDS ", 8) == 0 && curCfg->StepCount > 0) { int nLen = lineLen - 8; if (nLen >= (int)sizeof(curCfg->Steps[curCfg->StepCount - 1].Depends)) nLen = (int)sizeof(curCfg->Steps[curCfg->StepCount - 1].Depends) - 1; memcpy(curCfg->Steps[curCfg->StepCount - 1].Depends, lineStart + 8, nLen); curCfg->Steps[curCfg->StepCount - 1].Depends[nLen] = '\0'; } else if (lineLen > 8 && memcmp(lineStart, "COMMAND ", 8) == 0 && curCfg->StepCount > 0) { int nLen = lineLen - 8; if (nLen >= (int)sizeof(curCfg->Steps[curCfg->StepCount - 1].Command)) nLen = (int)sizeof(curCfg->Steps[curCfg->StepCount - 1].Command) - 1; memcpy(curCfg->Steps[curCfg->StepCount - 1].Command, lineStart + 8, nLen); curCfg->Steps[curCfg->StepCount - 1].Command[nLen] = '\0'; } } } while (*pos == '\n' || *pos == '\r') pos++; } bool anyBuilt = false; for (int i = 0; i < configCount; i++) { BuildConfig* cfg = &configs[i]; printf("\n==============================================================================\n"); printf("Generating Unity Files [%s]...\n", cfg->Name); printf("==============================================================================\n"); bool unityChanged = false; if (cfg->UnityStepCount > 0) { LARGE_INTEGER tStart, tEnd; QueryPerformanceCounter(&tStart); for (int u = 0; u < cfg->UnityStepCount; u++) { if (GenerateUnityFiles(&cfg->UnitySteps[u])) { unityChanged = true; } } QueryPerformanceCounter(&tEnd); char stepName[MAX_PATH_LEN]; sprintf_s(stepName, sizeof(stepName), "Unity Gen [%s]", cfg->Name); AppendTiming(stepName, (double)(tEnd.QuadPart - tStart.QuadPart) / (double)perfFreq.QuadPart); } if (unityChanged) { printf("[Unity] Source file list or include mapping changed.\n"); } else { printf("[Unity] No changes detected [up-to-date].\n"); } printf("\n==============================================================================\n"); printf("Compiling Targets [%s]...\n", cfg->Name); printf("==============================================================================\n"); g_DirCacheCount = 0; bool anyNeedBuild = false; for (int j = 0; j < cfg->StepCount; j++) { BuildStep* step = &cfg->Steps[j]; int64_t maxTime = 0; const char* cur = step->Depends; while (*cur) { const char* nextPipe = strchr(cur, '|'); char dirPath[MAX_PATH_LEN] = { 0 }; if (nextPipe) { int dLen = (int)(nextPipe - cur); if (dLen >= (int)sizeof(dirPath)) dLen = (int)sizeof(dirPath) - 1; memcpy(dirPath, cur, dLen); dirPath[dLen] = '\0'; cur = nextPipe + 1; } else { strcpy_s(dirPath, sizeof(dirPath), cur); cur += strlen(cur); } if (dirPath[0] != '\0') { int64_t t = GetDirTime(dirPath); if (t > maxTime) maxTime = t; } } bool needCompile = unityChanged; if (!needCompile) { if (!FileExists(step->OutputFile)) { needCompile = true; } else { int64_t outTime = GetFileModTimeInt64(step->OutputFile); if (outTime < maxTime) needCompile = true; } } printf("\n--- %s ---\n", step->StepName); if (!needCompile) { printf("[SKIP / UP-TO-DATE] %s [Binary up to date]\n", step->OutputFile); } else { printf("[REBUILDING] Compiling %s...\n", step->OutputFile); anyNeedBuild = true; LARGE_INTEGER tStart, tEnd; QueryPerformanceCounter(&tStart); int res = ExecuteCmd(step->Command); QueryPerformanceCounter(&tEnd); AppendTiming(step->StepName, (double)(tEnd.QuadPart - tStart.QuadPart) / (double)perfFreq.QuadPart); if (res != 0) { printf("[BUILD FAILED] Command failed with exit code %d.\n", res); return res; } char builtOut[MAX_PATH_LEN + 2]; sprintf_s(builtOut, sizeof(builtOut), "%s\n", step->OutputFile); HANDLE hFile = CreateFileA("Intermediate\\built_outputs.tmp", FILE_APPEND_DATA, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile != INVALID_HANDLE_VALUE) { DWORD written = 0; WriteFile(hFile, builtOut, (DWORD)strlen(builtOut), &written, NULL); CloseHandle(hFile); } anyBuilt = true; } } if (!anyNeedBuild) { printf("\n[SKIP / UP-TO-DATE] All requested targets in [%s] are up-to-date.\n", cfg->Name); } } if (g_TimingsLen > 0) { WriteEntireFile("Intermediate\\step_timings.tmp", g_TimingsBuf, g_TimingsLen); } QueryPerformanceCounter(&perfEnd); double elapsedSeconds = (double)(perfEnd.QuadPart - perfStart.QuadPart) / (double)perfFreq.QuadPart; char elapsedBuf[64]; if (elapsedSeconds < 60.0) { sprintf_s(elapsedBuf, sizeof(elapsedBuf), "%.3fs", elapsedSeconds); } else { int totalMs = (int)(elapsedSeconds * 1000.0); int hours = totalMs / 3600000; int minutes = (totalMs % 3600000) / 60000; int seconds = (totalMs % 60000) / 1000; int ms = totalMs % 1000; sprintf_s(elapsedBuf, sizeof(elapsedBuf), "%02d:%02d:%02d.%03d", hours, minutes, seconds, ms); } printf("\n==============================================================================\n"); printf("[BUILD SUMMARY]\n"); printf("==============================================================================\n"); if (g_TimingsLen > 0) { printf("Step Timings:\n"); const char* pos = g_TimingsBuf; while (*pos) { const char* lineStart = pos; while (*pos && *pos != '\n') pos++; int len = (int)(pos - lineStart); if (len > 0) { const char* pipe = strchr(lineStart, '|'); if (pipe && pipe < pos) { char stepName[MAX_PATH_LEN] = { 0 }; int nameLen = (int)(pipe - lineStart); if (nameLen >= MAX_PATH_LEN) nameLen = MAX_PATH_LEN - 1; memcpy(stepName, lineStart, nameLen); char durStr[64] = { 0 }; int durLen = (int)(pos - (pipe + 1)); if (durLen >= 64) durLen = 63; memcpy(durStr, pipe + 1, durLen); printf(" - %-42s : %s\n", stepName, durStr); } } if (*pos == '\n') pos++; } printf("------------------------------------------------------------------------------\n"); } printf("Total Duration : %s\n", elapsedBuf); printf("Output Binaries:\n"); if (anyBuilt) { FileContent outputsContent = ReadEntireFile("Intermediate\\built_outputs.tmp"); if (outputsContent.Data) { const char* pos = outputsContent.Data; while (*pos) { const char* lineStart = pos; while (*pos && *pos != '\n' && *pos != '\r') pos++; int len = (int)(pos - lineStart); if (len > 0) { char filePath[MAX_PATH_LEN] = { 0 }; if (len >= MAX_PATH_LEN) len = MAX_PATH_LEN - 1; memcpy(filePath, lineStart, len); WIN32_FILE_ATTRIBUTE_DATA fileData; if (GetFileAttributesExA(filePath, GetFileExInfoStandard, &fileData)) { ULARGE_INTEGER fileSize; fileSize.LowPart = fileData.nFileSizeLow; fileSize.HighPart = fileData.nFileSizeHigh; double sizeMB = (double)fileSize.QuadPart / (1024.0 * 1024.0); printf(" - %s (%.2f MB)\n", filePath, sizeMB); } } while (*pos == '\n' || *pos == '\r') pos++; } } DeleteFileA("Intermediate\\built_outputs.tmp"); } else { printf(" - None built in this session (all requested targets up-to-date)\n"); } printf("==============================================================================\n"); printf("[ALL BUILDS SUCCEEDED] All requested targets processed successfully.\n"); printf("==============================================================================\n"); fflush(stdout); return 0; } static void GenerateProject(const char* projectName, const char* projectDir, const char* guid, const char* includeSearchPath, const char* extraDefs, const char* debuggerCommandArgs, const char Srcs[64][256], int SrcCount, const char* defCommon, const char* defDebug, const char* defProfile, const char* defRelease) { FilePathList* srcFiles = CreateFilePathList(); if (SrcCount > 0) { for (int i = 0; i < SrcCount; i++) { FilePathListAdd(srcFiles, Srcs[i]); } } else { CollectFiles(projectDir, NULL, ".cpp", srcFiles); CollectFiles(projectDir, NULL, ".c", srcFiles); CollectFiles(projectDir, NULL, ".h", srcFiles); CollectFiles(projectDir, NULL, ".hpp", srcFiles); CollectFiles(projectDir, NULL, ".inl", srcFiles); } char* buf = (char*)ArenaPush(4 * 1024 * 1024); int offset = 0; offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, "\n" "\n" " \n" " \n" " Debug\n" " x64\n" " \n" " \n" " Profile\n" " x64\n" " \n" " \n" " Release\n" " x64\n" " \n" " \n" " \n" " {%s}\n" " MakeFileProj\n" " \n" " \n", guid); const char* configs[] = { "Debug", "Profile", "Release" }; for (int i = 0; i < 3; i++) { offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, " \n" " Makefile\n" " false\n" " v143\n" " \n", configs[i]); } offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, " \n" " \n" " \n"); for (int i = 0; i < 3; i++) { const char* cfg = configs[i]; offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, " \n" " cd $(SolutionDir) & misc\\build.bat %s $(SolutionName)\n" " cd $(SolutionDir) & misc\\build.bat -clean %s $(SolutionName)\n" " cd $(SolutionDir) & misc\\build.bat -clean %s $(SolutionName)\n" " $(SolutionDir)bin\\x64-%s\\%s.exe\n" " %s$(NMakeIncludeSearchPath)\n" " %s$(IncludePath)\n", cfg, cfg, cfg, cfg, cfg, projectName, includeSearchPath, includeSearchPath); offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, " "); if (defCommon && defCommon[0]) offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, "%s", defCommon); if (strcmp(cfg, "Debug") == 0 && defDebug && defDebug[0]) offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, "%s", defDebug); else if (strcmp(cfg, "Profile") == 0 && defProfile && defProfile[0]) offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, "%s", defProfile); else if (strcmp(cfg, "Release") == 0 && defRelease && defRelease[0]) offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, "%s", defRelease); if (extraDefs && extraDefs[0]) offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, "%s", extraDefs); offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, "$(NMakePreprocessorDefinitions)\n" " /std:c++20\n" " stdcpp20\n"); if (debuggerCommandArgs && debuggerCommandArgs[0] != '\0') { offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, " %s\n", debuggerCommandArgs); } offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, " $(SolutionDir)bin\\x64-%s\\\n" " $(SolutionDir)Intermediate\\x64-%s\\%s\\\n" " $(SolutionDir)bin\\x64-%s\\\n" " \n", cfg, cfg, projectName, cfg); } offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, " \n"); int projectDirLen = (int)strlen(projectDir); for (int i = 0; i < srcFiles->Count; i++) { const char* path = srcFiles->Paths[i]; const char* relPath = path; if (strncmp(path, projectDir, projectDirLen) == 0 && (path[projectDirLen] == '\\' || path[projectDirLen] == '/')) { relPath = path + projectDirLen + 1; } if (EndsWithNoCase(path, ".cpp") || EndsWithNoCase(path, ".c")) { offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, " \n", relPath); } else { offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, " \n", relPath); } } offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, " \n"); offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, " \n" " \n" " \n" "\n"); char outPath[MAX_PATH_LEN]; snprintf(outPath, sizeof(outPath), "%s\\%s.vcxproj", projectDir, projectName); WriteEntireFile(outPath, buf, offset); printf("Generated %s\n", outPath); // --- Generate .vcxproj.filters --- char filters[512][256]; int filterCount = 0; for (int i = 0; i < srcFiles->Count; i++) { const char* path = srcFiles->Paths[i]; const char* relPath = path; if (strncmp(path, projectDir, projectDirLen) == 0 && (path[projectDirLen] == '\\' || path[projectDirLen] == '/')) { relPath = path + projectDirLen + 1; } const char* lastSlash = strrchr(relPath, '\\'); if (!lastSlash) lastSlash = strrchr(relPath, '/'); if (lastSlash) { char dir[256]; int len = (int)(lastSlash - relPath); if (len >= 256) len = 255; memcpy(dir, relPath, len); dir[len] = '\0'; char cur[256]; int c = 0; for (int k = 0; k < len; k++) { cur[c++] = dir[k]; if (dir[k] == '\\' || dir[k] == '/') { cur[c - 1] = '\0'; bool found = false; for (int f = 0; f < filterCount; f++) { if (_stricmp(filters[f], cur) == 0) { found = true; break; } } if (!found && filterCount < 512) { strcpy_s(filters[filterCount++], 256, cur); } cur[c - 1] = '\\'; } } bool found = false; for (int f = 0; f < filterCount; f++) { if (_stricmp(filters[f], dir) == 0) { found = true; break; } } if (!found && filterCount < 512) { strcpy_s(filters[filterCount++], 256, dir); } } } char* filterBuf = (char*)ArenaPush(4 * 1024 * 1024); int filterOffset = 0; filterOffset += sprintf_s(filterBuf + filterOffset, (4 * 1024 * 1024) - filterOffset, "\n" "\n" " \n"); for (int i = 0; i < filterCount; i++) { filterOffset += sprintf_s(filterBuf + filterOffset, (4 * 1024 * 1024) - filterOffset, " \n" " \n", filters[i]); } filterOffset += sprintf_s(filterBuf + filterOffset, (4 * 1024 * 1024) - filterOffset, " \n" " \n"); for (int i = 0; i < srcFiles->Count; i++) { const char* path = srcFiles->Paths[i]; const char* relPath = path; if (strncmp(path, projectDir, projectDirLen) == 0 && (path[projectDirLen] == '\\' || path[projectDirLen] == '/')) { relPath = path + projectDirLen + 1; } const char* lastSlash = strrchr(relPath, '\\'); if (!lastSlash) lastSlash = strrchr(relPath, '/'); const char* type = (EndsWithNoCase(path, ".cpp") || EndsWithNoCase(path, ".c")) ? "ClCompile" : "ClInclude"; if (lastSlash) { char dir[256]; int len = (int)(lastSlash - relPath); if (len >= 256) len = 255; memcpy(dir, relPath, len); dir[len] = '\0'; filterOffset += sprintf_s(filterBuf + filterOffset, (4 * 1024 * 1024) - filterOffset, " <%s Include=\"%s\">\n" " %s\n" " \n", type, relPath, dir, type); } else { filterOffset += sprintf_s(filterBuf + filterOffset, (4 * 1024 * 1024) - filterOffset, " <%s Include=\"%s\" />\n", type, relPath); } } filterOffset += sprintf_s(filterBuf + filterOffset, (4 * 1024 * 1024) - filterOffset, " \n\n"); char filterOutPath[MAX_PATH_LEN]; snprintf(filterOutPath, sizeof(filterOutPath), "%s\\%s.vcxproj.filters", projectDir, projectName); WriteEntireFile(filterOutPath, filterBuf, filterOffset); printf("Generated %s\n", filterOutPath); } static void GenerateSolution(const char* slnName, const char** projectNames, const char** projectDirs, const char** projectGuids, int projCount) { char* buf = (char*)ArenaPush(4 * 1024 * 1024); int offset = 0; offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, "Microsoft Visual Studio Solution File, Format Version 12.00\n" "# Visual Studio Version 17\n" "VisualStudioVersion = 17.0.31903.59\n" "MinimumVisualStudioVersion = 10.0.40219.1\n"); for (int i = 0; i < projCount; i++) { offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, "Project(\"{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}\") = \"%s\", \"%s\\%s.vcxproj\", \"{%s}\"\n" "EndProject\n", projectNames[i], projectDirs[i], projectNames[i], projectGuids[i]); } offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, "Global\n" " GlobalSection(SolutionConfigurationPlatforms) = preSolution\n" " Debug|x64 = Debug|x64\n" " Profile|x64 = Profile|x64\n" " Release|x64 = Release|x64\n" " EndGlobalSection\n" " GlobalSection(ProjectConfigurationPlatforms) = postSolution\n"); for (int i = 0; i < projCount; i++) { const char* configs[] = { "Debug", "Profile", "Release" }; for (int c = 0; c < 3; c++) { offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, " {%s}.%s|x64.ActiveCfg = %s|x64\n" " {%s}.%s|x64.Build.0 = %s|x64\n", projectGuids[i], configs[c], configs[c], projectGuids[i], configs[c], configs[c]); } } offset += sprintf_s(buf + offset, (4 * 1024 * 1024) - offset, " EndGlobalSection\n" " GlobalSection(SolutionProperties) = preSolution\n" " HideSolutionNode = FALSE\n" " EndGlobalSection\n" "EndGlobal\n"); char outPath[MAX_PATH_LEN]; snprintf(outPath, sizeof(outPath), "%s.sln", slnName); WriteEntireFile(outPath, buf, offset); printf("Generated %s\n", outPath); } typedef struct { char Name[64]; char Dir[256]; char Guid[64]; char ExtraDef[1024]; char IncludeSearch[1024]; char DebugCmd[1024]; char Srcs[64][256]; int SrcCount; } VSProject; typedef struct { char Name[64]; char Projects[16][64]; int ProjCount; } VSSolution; static void GenerateDeterministicGuid(const char* input, char* outputGuid) { unsigned long long hash1 = 14695981039346656037ULL; unsigned long long hash2 = 1099511628211ULL; for (const char* c = input; *c; ++c) { hash1 ^= (unsigned long long)(*c); hash1 *= 1099511628211ULL; hash2 ^= (unsigned long long)(*c); hash2 *= 137ULL; } sprintf_s(outputGuid, 64, "%08x-%04x-%04x-%04x-%012llx", (unsigned int)(hash1 >> 32), (unsigned int)((hash1 >> 16) & 0xFFFF), (unsigned int)(hash1 & 0xFFFF), (unsigned int)((hash2 >> 48) & 0xFFFF), hash2 & 0xFFFFFFFFFFFFULL); } static int CommandGenVS(int argc, char* argv[]) { if (argc < 3) { printf("[ERROR] Usage: build_system gen_vs \n"); return 1; } const char* planFile = argv[2]; FileContent planContent = ReadEntireFile(planFile); if (!planContent.Data) { printf("[ERROR] Could not read VS plan: %s\n", planFile); return 1; } char globalInclude[1024] = {0}; char globalDefCommon[1024] = {0}; char globalDefDebug[1024] = {0}; char globalDefProfile[1024] = {0}; char globalDefRelease[1024] = {0}; VSProject projects[16]; int projCount = 0; VSSolution solutions[8]; int slnCount = 0; const char* pos = planContent.Data; while (*pos) { const char* lineStart = pos; while (*pos && *pos != '\n' && *pos != '\r') pos++; int lineLen = (int)(pos - lineStart); if (lineLen > 0) { if (lineLen > 15 && memcmp(lineStart, "GLOBAL_INCLUDE ", 15) == 0) { int nLen = lineLen - 15; if (nLen >= 1024) nLen = 1023; memcpy(globalInclude, lineStart + 15, nLen); globalInclude[nLen] = '\0'; } else if (lineLen > 11 && memcmp(lineStart, "DEF_COMMON ", 11) == 0) { int nLen = lineLen - 11; if (nLen >= 1024) nLen = 1023; memcpy(globalDefCommon, lineStart + 11, nLen); globalDefCommon[nLen] = '\0'; } else if (lineLen > 10 && memcmp(lineStart, "DEF_DEBUG ", 10) == 0) { int nLen = lineLen - 10; if (nLen >= 1024) nLen = 1023; memcpy(globalDefDebug, lineStart + 10, nLen); globalDefDebug[nLen] = '\0'; } else if (lineLen > 12 && memcmp(lineStart, "DEF_PROFILE ", 12) == 0) { int nLen = lineLen - 12; if (nLen >= 1024) nLen = 1023; memcpy(globalDefProfile, lineStart + 12, nLen); globalDefProfile[nLen] = '\0'; } else if (lineLen > 12 && memcmp(lineStart, "DEF_RELEASE ", 12) == 0) { int nLen = lineLen - 12; if (nLen >= 1024) nLen = 1023; memcpy(globalDefRelease, lineStart + 12, nLen); globalDefRelease[nLen] = '\0'; } else if (lineLen > 8 && memcmp(lineStart, "PROJECT ", 8) == 0) { if (projCount < 16) { VSProject* p = &projects[projCount++]; memset(p, 0, sizeof(VSProject)); char lineBuf[1024]; int nLen = lineLen - 8; if (nLen >= 1024) nLen = 1023; memcpy(lineBuf, lineStart + 8, nLen); lineBuf[nLen] = '\0'; int scanned = sscanf_s(lineBuf, "%s %s %s", p->Name, (unsigned)_countof(p->Name), p->Dir, (unsigned)_countof(p->Dir), p->Guid, (unsigned)_countof(p->Guid)); if (scanned < 2) { strcpy_s(p->Dir, sizeof(p->Dir), p->Name); } if (scanned < 3) { GenerateDeterministicGuid(p->Name, p->Guid); } } } else if (lineLen > 12 && memcmp(lineStart, "PROJECT_SRC ", 12) == 0 && projCount > 0) { VSProject* p = &projects[projCount - 1]; if (p->SrcCount < 64) { int nLen = lineLen - 12; if (nLen >= 256) nLen = 255; memcpy(p->Srcs[p->SrcCount], lineStart + 12, nLen); p->Srcs[p->SrcCount][nLen] = '\0'; p->SrcCount++; } } else if (lineLen > 10 && memcmp(lineStart, "EXTRA_DEF ", 10) == 0 && projCount > 0) { VSProject* p = &projects[projCount - 1]; int nLen = lineLen - 10; if (nLen >= 1024) nLen = 1023; memcpy(p->ExtraDef, lineStart + 10, nLen); p->ExtraDef[nLen] = '\0'; } else if (lineLen > 15 && memcmp(lineStart, "INCLUDE_SEARCH ", 15) == 0 && projCount > 0) { VSProject* p = &projects[projCount - 1]; int nLen = lineLen - 15; if (nLen >= 1024) nLen = 1023; memcpy(p->IncludeSearch, lineStart + 15, nLen); p->IncludeSearch[nLen] = '\0'; } else if (lineLen > 10 && memcmp(lineStart, "DEBUG_CMD ", 10) == 0 && projCount > 0) { VSProject* p = &projects[projCount - 1]; int nLen = lineLen - 10; if (nLen >= 1024) nLen = 1023; memcpy(p->DebugCmd, lineStart + 10, nLen); p->DebugCmd[nLen] = '\0'; } else if (lineLen > 9 && memcmp(lineStart, "SOLUTION ", 9) == 0) { if (slnCount < 8) { VSSolution* s = &solutions[slnCount++]; memset(s, 0, sizeof(VSSolution)); int nLen = lineLen - 9; if (nLen >= 64) nLen = 63; memcpy(s->Name, lineStart + 9, nLen); s->Name[nLen] = '\0'; } } else if (lineLen > 9 && memcmp(lineStart, "SLN_PROJ ", 9) == 0 && slnCount > 0) { VSSolution* s = &solutions[slnCount - 1]; if (s->ProjCount < 16) { int nLen = lineLen - 9; if (nLen >= 64) nLen = 63; memcpy(s->Projects[s->ProjCount], lineStart + 9, nLen); s->Projects[s->ProjCount][nLen] = '\0'; s->ProjCount++; } } } while (*pos == '\n' || *pos == '\r') pos++; } for (int i = 0; i < projCount; i++) { VSProject* p = &projects[i]; const char* incs = p->IncludeSearch[0] != '\0' ? p->IncludeSearch : globalInclude; GenerateProject(p->Name, p->Dir, p->Guid, incs, p->ExtraDef, p->DebugCmd, p->Srcs, p->SrcCount, globalDefCommon, globalDefDebug, globalDefProfile, globalDefRelease); } for (int i = 0; i < slnCount; i++) { VSSolution* s = &solutions[i]; const char* pNames[16]; const char* pDirs[16]; const char* pGuids[16]; for (int j = 0; j < s->ProjCount; j++) { pNames[j] = s->Projects[j]; for (int k = 0; k < projCount; k++) { if (strcmp(projects[k].Name, s->Projects[j]) == 0) { pDirs[j] = projects[k].Dir; pGuids[j] = projects[k].Guid; break; } } } GenerateSolution(s->Name, pNames, pDirs, pGuids, s->ProjCount); } return 0; } int main(int argc, char* argv[]) { // 256 MB Bump allocator ArenaInit(256 * 1024 * 1024); if (argc < 2) { printf("Usage: build_system [args...]\n"); printf("Commands:\n"); printf(" run_plan Run the build from a generated plan file\n"); printf(" gen_vs Generate Visual Studio project and solution files\n"); printf(" check_alive Returns 0 immediately to test execution\n"); return 1; } const char* cmd = argv[1]; if (strcmp(cmd, "check_alive") == 0) return 0; if (strcmp(cmd, "run_plan") == 0) return CommandRunPlan(argc, argv); if (strcmp(cmd, "gen_vs") == 0) return CommandGenVS(argc, argv); printf("[ERROR] Unknown command: %s\n", cmd); return 1; }