From 879078eb99c8bc38c40c1953c7416c38a822e64a Mon Sep 17 00:00:00 2001 From: Simeon Armenchev Date: Sun, 21 Jul 2024 03:53:09 +0300 Subject: [PATCH] refactor(mcl/utils/json): Refactor and format --- flake.lock | 14 +- flake.nix | 2 +- packages/mcl/src/main.d | 1 - packages/mcl/src/src/mcl/commands/ci.d | 109 +++-- packages/mcl/src/src/mcl/commands/ci_matrix.d | 282 +++++++----- packages/mcl/src/src/mcl/commands/get_fstab.d | 1 - packages/mcl/src/src/mcl/commands/host_info.d | 424 ++++++++---------- .../mcl/src/src/mcl/commands/machine_create.d | 252 ++++++----- .../mcl/src/src/mcl/commands/shard_matrix.d | 2 - packages/mcl/src/src/mcl/utils/array.d | 27 +- packages/mcl/src/src/mcl/utils/coda.d | 26 +- packages/mcl/src/src/mcl/utils/json.d | 97 ++-- packages/mcl/src/src/mcl/utils/path.d | 5 +- packages/mcl/test.sh | 2 +- 14 files changed, 630 insertions(+), 614 deletions(-) diff --git a/flake.lock b/flake.lock index 432f5292..0e8a5bde 100644 --- a/flake.lock +++ b/flake.lock @@ -212,17 +212,17 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1714055636, - "narHash": "sha256-8LCyIPAK4/4ge03ohCIWpoJrMGgoCaOriALzt7gPxHE=", + "lastModified": 1719092179, + "narHash": "sha256-/4jxq5+pDkVMao5RzOm27C2AfANjisTuvsycA0pbBCg=", "owner": "PetarKirov", "repo": "dlang.nix", - "rev": "dab4c199ad644dc23b0b9481e2e5a063e9492b84", + "rev": "21b1b3b18b3b635a43b319612aff529d26b1863b", "type": "github" }, "original": { "owner": "PetarKirov", "repo": "dlang.nix", - "rev": "dab4c199ad644dc23b0b9481e2e5a063e9492b84", + "rev": "21b1b3b18b3b635a43b319612aff529d26b1863b", "type": "github" } }, @@ -814,11 +814,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1711703276, - "narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=", + "lastModified": 1719506693, + "narHash": "sha256-C8e9S7RzshSdHB7L+v9I51af1gDM5unhJ2xO1ywxNH8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d8fe5e6c92d0d190646fb9f1056741a229980089", + "rev": "b2852eb9365c6de48ffb0dc2c9562591f652242a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 0244aa27..683b12fe 100644 --- a/flake.nix +++ b/flake.nix @@ -197,7 +197,7 @@ }; dlang-nix = { - url = "github:PetarKirov/dlang.nix?branch=feat/build-dub-package&rev=dab4c199ad644dc23b0b9481e2e5a063e9492b84"; + url = "github:PetarKirov/dlang.nix?branch=feat/build-dub-package&rev=21b1b3b18b3b635a43b319612aff529d26b1863b"; inputs = { flake-compat.follows = "flake-compat"; flake-parts.follows = "flake-parts"; diff --git a/packages/mcl/src/main.d b/packages/mcl/src/main.d index 9b3b4f6e..e26e3a80 100644 --- a/packages/mcl/src/main.d +++ b/packages/mcl/src/main.d @@ -41,7 +41,6 @@ int main(string[] args) cmd(); info("Execution Succesfull"); return 0; - } } catch (Exception e) diff --git a/packages/mcl/src/src/mcl/commands/ci.d b/packages/mcl/src/src/mcl/commands/ci.d index 206da19d..344d2bbd 100644 --- a/packages/mcl/src/src/mcl/commands/ci.d +++ b/packages/mcl/src/src/mcl/commands/ci.d @@ -3,14 +3,14 @@ module mcl.commands.ci; import std.file : readText; import std.json : parseJSON, JSONValue; import std.stdio : writeln, write; -import std.algorithm : map; +import std.algorithm : map, each; import std.array : array, join; import std.conv : to; import std.process : ProcessPipes; import mcl.utils.env : optional, parseEnv; -import mcl.commands.ci_matrix : nixEvalJobs, SupportedSystem, Params; -import mcl.commands.shard_matrix : generateShardMatrix; +import mcl.commands.ci_matrix : nixEvalJobs, SupportedSystem, Params, Package; +import mcl.commands.shard_matrix : generateShardMatrix, Shard; import mcl.utils.path : rootDir, createResultDirs; import mcl.utils.process : execute; import mcl.utils.nix : nix; @@ -23,64 +23,59 @@ export void ci() params = parseEnv!Params; auto shardMatrix = generateShardMatrix(); - foreach (shard; shardMatrix.include) - { - writeln("Shard ", shard.prefix ~ " ", shard.postfix ~ " ", shard.digit); - params.flakePre = shard.prefix; - params.flakePost = shard.postfix; + shardMatrix.include.each!(handleShard); +} - if (params.flakePre == "") - { - params.flakePre = "checks"; - } - if (params.flakePost != "") - { - params.flakePost = "." ~ params.flakePost; - } - string cachixUrl = "https://" ~ params.cachixCache ~ ".cachix.org"; - version (AArch64) - { - string arch = "aarch64"; - } - version (X86_64) - { - string arch = "x86_64"; - } +static immutable(SupportedSystem) platform() +{ + version (AArch64) + static immutable string arch = "aarch64"; + else version (X86_64) + static immutable string arch = "x86_64"; - version (linux) - { - string os = "linux"; - } - version (OSX) - { - string os = "darwin"; - } + version (linux) + static immutable string os = "linux"; + else version (OSX) + static immutable string os = "darwin"; - auto matrix = nixEvalJobs(params, (arch ~ "_" ~ os).to!(SupportedSystem), cachixUrl, false); - foreach (pkg; matrix) - { - if (pkg.isCached) - { - writeln("Package ", pkg.name, " is cached"); - } - else - { - writeln("Package ", pkg.name, " is not cached; building..."); - ProcessPipes res = execute!ProcessPipes([ - "nix", "build", "--json", ".#" ~ pkg.attrPath - ]); + return (arch ~ "_" ~ os).to!(SupportedSystem); +} - foreach (line; res.stderr.byLine) - { - "\r".write; - line.write; - } - "".writeln; - auto json = parseJSON(res.stdout.byLine.join("\n").to!string); - auto path = json.array[0]["outputs"]["out"].str; - execute(["cachix", "push", params.cachixCache, path], false, true).writeln; - } - } +void handleShard(Shard shard) +{ + writeln("Shard ", shard.prefix ~ " ", shard.postfix ~ " ", shard.digit); + params.flakePre = shard.prefix; + params.flakePost = shard.postfix; + + if (params.flakePre == "") + params.flakePre = "checks"; + if (params.flakePost != "") + params.flakePost = "." ~ params.flakePost; + string cachixUrl = "https://" ~ params.cachixCache ~ ".cachix.org"; + auto matrix = nixEvalJobs(params, platform, cachixUrl, false); + matrix.each!(handlePackage); +} + +void handlePackage(Package pkg) +{ + if (pkg.isCached) + writeln("Package ", pkg.name, " is cached"); + else + { + writeln("Package ", pkg.name, " is not cached; building..."); + ProcessPipes res = execute!ProcessPipes([ + "nix", "build", "--json", ".#" ~ pkg.attrPath + ]); + + foreach (line; res.stderr.byLine) + { + "\r".write; + line.write; + } + "".writeln; + auto json = parseJSON(res.stdout.byLine.join("\n").to!string); + auto path = json.array[0]["outputs"]["out"].str; + execute(["cachix", "push", params.cachixCache, path], false, true).writeln; } } diff --git a/packages/mcl/src/src/mcl/commands/ci_matrix.d b/packages/mcl/src/src/mcl/commands/ci_matrix.d index 8f7ee8f8..7d99b403 100755 --- a/packages/mcl/src/src/mcl/commands/ci_matrix.d +++ b/packages/mcl/src/src/mcl/commands/ci_matrix.d @@ -12,7 +12,7 @@ import std.regex : matchFirst; import core.cpuid : threadsPerCPU; import std.path : buildPath; import std.process : pipeProcess, wait, Redirect, kill; -import std.exception : enforce; +import std.exception : enforce, assumeUnique; import std.format : fmt = format; import std.logger : tracef, infof; @@ -25,6 +25,7 @@ import mcl.utils.process : execute; enum GitHubOS { @StringRepresentation("ubuntu-latest") ubuntuLatest, + @StringRepresentation("self-hosted") selfHosted, @StringRepresentation("macos-14") macos14 @@ -39,19 +40,26 @@ enum SupportedSystem @StringRepresentation("aarch64-darwin") aarch64_darwin } +immutable GitHubOS[string] osMap; + +shared static this() +{ + import std.exception : assumeUnique; + import std.conv : to; + + GitHubOS[string] temp = [ + "ubuntu-latest": GitHubOS.ubuntuLatest, + "self-hosted": GitHubOS.selfHosted, + "macos-14": GitHubOS.macos14 + ]; + temp.rehash; + + osMap = assumeUnique(temp); +} + GitHubOS getGHOS(string os) { - switch (os) - { - case "self-hosted": - return GitHubOS.selfHosted; - case "ubuntu-latest": - return GitHubOS.ubuntuLatest; - case "macos-14": - return GitHubOS.macos14; - default: - return GitHubOS.selfHosted; - } + return os in osMap ? osMap[os] : GitHubOS.selfHosted; } @("getGHOS") @@ -62,19 +70,26 @@ unittest assert(getGHOS("crazyos-inator-2000") == GitHubOS.selfHosted); } +immutable SupportedSystem[string] systemMap; + +shared static this() +{ + import std.exception : assumeUnique; + import std.conv : to; + + SupportedSystem[string] temp = [ + "x86_64-linux": SupportedSystem.x86_64_linux, + "x86_64-darwin": SupportedSystem.x86_64_darwin, + "aarch64-darwin": SupportedSystem.aarch64_darwin + ]; + temp.rehash; + + systemMap = assumeUnique(temp); +} + SupportedSystem getSystem(string system) { - switch (system) - { - case "x86_64-linux": - return SupportedSystem.x86_64_linux; - case "x86_64-darwin": - return SupportedSystem.x86_64_darwin; - case "aarch64-darwin": - return SupportedSystem.aarch64_darwin; - default: - return SupportedSystem.x86_64_linux; - } + return system in systemMap ? systemMap[system] : SupportedSystem.x86_64_linux; } @("getSystem") @@ -130,12 +145,23 @@ struct SummaryTableEntry SummaryTableEntry_aarch64 aarch64; } +enum Status : string +{ + cached = "✅ cached", + notSupported = "🚫 not supported", + building = "⏳ building...", + buildFailed = "❌ build failed" +} + version (unittest) { static immutable SummaryTableEntry[] testSummaryTableEntryArray = [ - SummaryTableEntry("testPackage", SummaryTableEntry_x86_64("✅ cached", "✅ cached"), SummaryTableEntry_aarch64("🚫 not supported")), - SummaryTableEntry("testPackage2", SummaryTableEntry_x86_64("⏳ building...", "❌ build failed"), SummaryTableEntry_aarch64( - "⏳ building...")) + SummaryTableEntry("testPackage", + SummaryTableEntry_x86_64(Status.cached, Status.cached), + SummaryTableEntry_aarch64(Status.notSupported)), + SummaryTableEntry("testPackage2", + SummaryTableEntry_x86_64(Status.building, Status.buildFailed), + SummaryTableEntry_aarch64(Status.building)) ]; } @@ -200,10 +226,8 @@ struct Params } } -GitHubOS systemToGHPlatform(SupportedSystem os) -{ - return os == SupportedSystem.x86_64_linux ? GitHubOS.selfHosted : GitHubOS.macos14; -} +GitHubOS systemToGHPlatform(SupportedSystem os) => + os == SupportedSystem.x86_64_linux ? GitHubOS.selfHosted : GitHubOS.macos14; @("systemToGHPlatform") unittest @@ -283,12 +307,13 @@ Package[] nixEvalJobs(Params params, SupportedSystem system, string cachixUrl, b @MaxWidth(80) string output; } - Output( + Output output = { isCached: pkg.isCached, os: pkg.os, attr: pkg.attrPath, output: pkg.output - ).writeRecordAsTable(stderr.lockingTextWriter); + }; + output.writeRecordAsTable(stderr.lockingTextWriter); } } foreach (line; pipes.stderr.byLine) @@ -335,41 +360,39 @@ Package[] nixEvalForAllSystems() .array; } +static const MAX_WORKERS = 8; + int getNixEvalWorkerCount() { - return params.maxWorkers == 0 ? (threadsPerCPU() < 8 ? threadsPerCPU() : 8) : params.maxWorkers; + return params.maxWorkers == 0 ? (threadsPerCPU() < MAX_WORKERS ? threadsPerCPU() : MAX_WORKERS) + : params.maxWorkers; } @("getNixEvalWorkerCount") unittest { - assert(getNixEvalWorkerCount() == (threadsPerCPU() < 8 ? threadsPerCPU() : 8)); + assert(getNixEvalWorkerCount() == (threadsPerCPU() < MAX_WORKERS ? threadsPerCPU() : MAX_WORKERS)); } -int getAvailableMemoryMB() -{ +string[] meminfo; - // free="$(< /proc/meminfo grep MemFree | tr -s ' ' | cut -d ' ' -f 2)" - int free = "/proc/meminfo".readText - .splitLines - .find!(a => a.indexOf("MemFree") != -1) - .front - .split[1].to!int; - int cached = "/proc/meminfo".readText - .splitLines - .find!(a => a.indexOf("Cached") != -1 && a.indexOf("SwapCached") == -1) - .front - .split[1].to!int; - int buffers = "/proc/meminfo".readText - .splitLines - .find!(a => a.indexOf("Buffers") != -1) - .front - .split[1].to!int; - int shmem = "/proc/meminfo".readText - .splitLines - .find!(a => a.indexOf("Shmem:") != -1) +int getMemoryStat(string statName, string excludeName = "EXCLUDE") +{ + return meminfo + .find!(a => a.indexOf(statName) != -1 && a.indexOf(excludeName) == -1) .front .split[1].to!int; +} + +int getAvailableMemoryMB() +{ + meminfo = "/proc/meminfo".readText + .splitLines; + + int free = getMemoryStat("MemFree"); + int cached = getMemoryStat("Cached", "SwapCached"); + int buffers = getMemoryStat("Buffers"); + int shmem = getMemoryStat("Shmem:"); int maxMemoryMB = params.maxMemory == 0 ? ((free + cached + buffers + shmem) / 1024) : params.maxMemory; return maxMemoryMB; @@ -378,7 +401,13 @@ int getAvailableMemoryMB() @("getAvailableMemoryMB") unittest { + // Test when params.maxMemory is 0 + params.maxMemory = 0; assert(getAvailableMemoryMB() > 0); + + // Test when params.maxMemory is not 0 + params.maxMemory = 1024; + assert(getAvailableMemoryMB() == 1024); } void saveCachixDeploySpec(Package[] packages) @@ -407,9 +436,19 @@ unittest void saveGHCIMatrix(Package[] packages) { - auto matrix = JSONValue([ + auto matrix = createMatrix(packages); + writeMatrix(matrix); +} + +JSONValue createMatrix(Package[] packages) +{ + return JSONValue([ "include": JSONValue(packages.map!(pkg => pkg.toJSON()).array) ]); +} + +void writeMatrix(JSONValue matrix) +{ string resPath = rootDir.buildPath(params.isInitial ? "matrix-pre.json" : "matrix-post.json"); resPath.write(JSONValue(matrix).toString(JSONOptions.doNotEscapeSlashes)); } @@ -425,13 +464,20 @@ unittest .buildPath(params.isInitial ? "matrix-pre.json" : "matrix-post.json") .readText .parseJSON; - assert(testPackageArray[0].name == matrix["include"][0]["name"].str); + foreach (i, pkg; testPackageArray) + { + assert(pkg.name == matrix["include"][i]["name"].str); + } } void saveGHCIComment(SummaryTableEntry[] tableSummaryJSON) { - import std.path : buildNormalizedPath, absolutePath; + string comment = createComment(tableSummaryJSON); + writeComment(comment); +} +string createComment(SummaryTableEntry[] tableSummaryJSON) +{ string comment = "Thanks for your Pull Request!"; comment ~= "\n\nBelow you will find a summary of the cachix status of each package, for each supported platform."; comment ~= "\n\n| package | `x86_64-linux` | `x86_64-darwin` | `aarch64-darwin` |"; @@ -439,6 +485,12 @@ void saveGHCIComment(SummaryTableEntry[] tableSummaryJSON) comment ~= tableSummaryJSON.map!( pkg => "\n| " ~ pkg.name ~ " | " ~ pkg.x86_64.linux ~ " | " ~ pkg.x86_64.darwin ~ " | " ~ pkg.aarch64.darwin ~ " |") .join(""); + return comment; +} + +void writeComment(string comment) +{ + import std.path : buildNormalizedPath, absolutePath; auto outputPath = rootDir.buildNormalizedPath("comment.md"); write(outputPath, comment); @@ -468,49 +520,48 @@ string getStatus(JSONValue pkg, string key) { if (pkg[key]["isCached"].boolean) { - return "[✅ cached](" ~ pkg[key]["cacheUrl"].str ~ ")"; + return "[" ~ Status.cached ~ "](" ~ pkg[key]["cacheUrl"].str ~ ")"; } else if (params.isInitial) { - return "⏳ building..."; + return Status.building; } else { - return "❌ build failed"; + return Status.buildFailed; } } else { - return "🚫 not supported"; + return Status.notSupported; } } SummaryTableEntry[] convertNixEvalToTableSummary(Package[] packages) { - - SummaryTableEntry[] tableSummary = packages + return packages .chunkBy!((a, b) => a.name == b.name) - .map!((group) { - JSONValue pkg; - string name = group.array.front.name; - pkg["name"] = JSONValue(name); - foreach (item; group) - { - pkg[item.system.to!string] = item.toJSON(); - } - SummaryTableEntry entry = { - name, { - getStatus(pkg, "x86_64_linux"), getStatus(pkg, "x86_64_darwin") - }, { - getStatus(pkg, "aarch64_darwin") - } - }; - return entry; - }) + .map!(group => createSummaryTableEntry(group.array)) .array .sort!((a, b) => a.name < b.name) .release; - return tableSummary; +} + +SummaryTableEntry createSummaryTableEntry(Package[] group) +{ + JSONValue pkg; + string name = group.front.name; + pkg["name"] = JSONValue(name); + foreach (item; group) + { + pkg[item.system.to!string] = item.toJSON(); + } + SummaryTableEntry entry = { + name, {getStatus(pkg, "x86_64_linux"), getStatus(pkg, "x86_64_darwin")}, { + getStatus(pkg, "aarch64_darwin") + } + }; + return entry; } @("convertNixEvalToTableSummary/getStatus") @@ -518,26 +569,25 @@ unittest { auto tableSummary = convertNixEvalToTableSummary(cast(Package[]) testPackageArray); assert(tableSummary[0].name == testPackageArray[0].name); - assert(tableSummary[0].x86_64.linux == "[✅ cached](https://testPackage.com)"); - assert(tableSummary[0].x86_64.darwin == "🚫 not supported"); - assert(tableSummary[0].aarch64.darwin == "🚫 not supported"); + assert(tableSummary[0].x86_64.linux == "[" ~ Status.cached ~ "](https://testPackage.com)"); + assert(tableSummary[0].x86_64.darwin == Status.notSupported); + assert(tableSummary[0].aarch64.darwin == Status.notSupported); assert(tableSummary[1].name == testPackageArray[1].name); - assert(tableSummary[1].x86_64.linux == "🚫 not supported"); - assert(tableSummary[1].x86_64.darwin == "🚫 not supported"); - assert(tableSummary[1].aarch64.darwin == "❌ build failed"); + assert(tableSummary[1].x86_64.linux == Status.notSupported); + assert(tableSummary[1].x86_64.darwin == Status.notSupported); + assert(tableSummary[1].aarch64.darwin == Status.buildFailed); params.isInitial = true; tableSummary = convertNixEvalToTableSummary(cast(Package[]) testPackageArray); params.isInitial = false; assert(tableSummary[0].name == testPackageArray[0].name); - assert(tableSummary[0].x86_64.linux == "[✅ cached](https://testPackage.com)"); - assert(tableSummary[0].x86_64.darwin == "🚫 not supported"); - assert(tableSummary[0].aarch64.darwin == "🚫 not supported"); + assert(tableSummary[0].x86_64.linux == "[" ~ Status.cached ~ "](https://testPackage.com)"); + assert(tableSummary[0].x86_64.darwin == Status.notSupported); + assert(tableSummary[0].aarch64.darwin == Status.notSupported); assert(tableSummary[1].name == testPackageArray[1].name); - assert(tableSummary[1].x86_64.linux == "🚫 not supported"); - assert(tableSummary[1].x86_64.darwin == "🚫 not supported"); - assert(tableSummary[1].aarch64.darwin == "⏳ building..."); - + assert(tableSummary[1].x86_64.linux == Status.notSupported); + assert(tableSummary[1].x86_64.darwin == Status.notSupported); + assert(tableSummary[1].aarch64.darwin == Status.building); } void printTableForCacheStatus(Package[] packages) @@ -554,7 +604,7 @@ Package checkPackage(Package pkg) { import std.algorithm : canFind; import std.string : lineSplitter; - import std.net.curl : HTTP, httpGet = get, HTTPStatusException; + import std.net.curl : HTTP, httpGet = get, HTTPStatusException, CurlException; auto http = HTTP(); http.addRequestHeader("Authorization", "Bearer " ~ params.cachixAuthToken); @@ -569,6 +619,11 @@ Package checkPackage(Package pkg) { pkg.isCached = false; } + catch (CurlException e) + { + // Handle network errors + pkg.isCached = false; + } return pkg; } @@ -580,11 +635,10 @@ unittest const storePathHash = "mdb034kf7sq6g03ric56jxr4a7043l41"; const storePath = "/nix/store/" ~ storePathHash ~ "-hello-2.12.1"; - auto testPackage = Package( -output : storePath, -cacheUrl: - nixosCacheEndpoint ~ storePathHash ~ ".narinfo", - ); + Package testPackage = { + output: storePath, + cacheUrl: nixosCacheEndpoint ~ storePathHash ~ ".narinfo", + }; assert(!testPackage.isCached); assert(checkPackage(testPackage).isCached); @@ -610,8 +664,26 @@ Package[] getPrecalcMatrix() isCached: pkg["isCached"].boolean, os: getGHOS(pkg["os"].str), system: getSystem(pkg["system"].str), - output: pkg["output"].str}; - return result; - }).array; + output: pkg["output"].str + }; + return result; + }).array; - } +} + +@("getPrecalcMatrix") +unittest +{ + string precalcMatrixStr = "{\"include\": [{\"name\": \"test\", \"allowedToFail\": false, \"attrPath\": \"test\", \"cacheUrl\": \"url\", \"isCached\": true, \"os\": \"linux\", \"system\": \"x86_64-linux\", \"output\": \"output\"}]}"; + params.precalcMatrix = precalcMatrixStr; + auto packages = getPrecalcMatrix(); + assert(packages.length == 1); + assert(packages[0].name == "test"); + assert(!packages[0].allowedToFail); + assert(packages[0].attrPath == "test"); + assert(packages[0].cacheUrl == "url"); + assert(packages[0].isCached); + assert(packages[0].os == GitHubOS.selfHosted); + assert(packages[0].system == SupportedSystem.x86_64_linux); + assert(packages[0].output == "output"); +} diff --git a/packages/mcl/src/src/mcl/commands/get_fstab.d b/packages/mcl/src/src/mcl/commands/get_fstab.d index 032f79f3..aaa781ae 100755 --- a/packages/mcl/src/src/mcl/commands/get_fstab.d +++ b/packages/mcl/src/src/mcl/commands/get_fstab.d @@ -37,7 +37,6 @@ struct Params void setup() { - cachixStoreUrl = cachixNixStoreUrl(cachixCache); if (!cachixDeployWorkspace) cachixDeployWorkspace = cachixCache; diff --git a/packages/mcl/src/src/mcl/commands/host_info.d b/packages/mcl/src/src/mcl/commands/host_info.d index 1b503fec..de314932 100644 --- a/packages/mcl/src/src/mcl/commands/host_info.d +++ b/packages/mcl/src/src/mcl/commands/host_info.d @@ -7,70 +7,44 @@ import std.system; import std.stdio : writeln; import std.conv : to; -import std.string : strip, indexOf, isNumeric; -import std.array : split, join, array, replace; -import std.algorithm : map, filter, startsWith, joiner, any, sum; +import std.string : strip, indexOf, isNumeric, splitLines; +import std.array : split, join, array, replace, assocArray; +import std.algorithm : map, filter, startsWith, joiner, any, sum, canFind, all; import std.file : exists, write, readText, readLink, dirEntries, SpanMode; import std.path : baseName; import std.json; import std.process : ProcessPipes, environment; +import std.typecons : tuple; import core.stdc.string : strlen; import mcl.utils.env : parseEnv, optional; -import mcl.utils.json : toJSON; +import mcl.utils.json : toJSON, getStrOrDefault; import mcl.utils.process : execute, isRoot; import mcl.utils.number : humanReadableSize; import mcl.utils.array : uniqIfSame; import mcl.utils.nix : Literal; -// enum InfoFormat -// { -// JSON, -// CSV, -// TSV -// } - -struct Params -{ - // @optional() - // InfoFormat format = InfoFormat.JSON; - void setup() - { - } -} - string[string] cpuinfo; string[string] meminfo; string[string] getProcInfo(string fileOrData, bool file = true) { - string[string] r; - foreach (line; file ? fileOrData.readText().split( - "\n").map!(strip).array - : fileOrData.split("\n").map!(strip).array) - { - if (line.indexOf(":") == -1 || line.strip == "edid-decode (hex):") - { - continue; - } - auto parts = line.split(":"); - if (parts.length >= 2 && parts[0].strip != "") - { - r[parts[0].strip] = parts[1].strip; - } - } - return r; + auto lines = file ? fileOrData.readText().splitLines : fileOrData.splitLines; + return lines + .map!(strip) + .filter!(line => line.canFind(":") && line.strip != "edid-decode (hex):") + .map!(line => line.split(":")) + .filter!(parts => parts.length >= 2 && parts[0].strip != "") + .map!(parts => tuple(parts[0].strip, parts[1].strip)) + .assocArray; } export void host_info() { - const params = parseEnv!Params; - Info info = getInfo(); writeln(info.toJSON(true).toPrettyString(JSONOptions.doNotEscapeSlashes)); - } Info getInfo() @@ -149,31 +123,18 @@ string getOpMode() case ISA.msp430: opMode = [16]; break; - case ISA.x86_64: - case ISA.aarch64: - case ISA.ppc64: - case ISA.mips64: - case ISA.nvptx64: - case ISA.riscv64: - case ISA.sparc64: - case ISA.hppa64: + case ISA.x86_64, ISA.aarch64, ISA.ppc64: + case ISA.mips64, ISA.nvptx64, ISA.riscv64: + case ISA.sparc64, ISA.hppa64: opMode = [32, 64]; break; - case ISA.x86: - case ISA.arm: - case ISA.ppc: - case ISA.mips32: - case ISA.nvptx: - case ISA.riscv32: - case ISA.sparc: - case ISA.s390: - case ISA.hppa: - case ISA.sh: - case ISA.webAssembly: + case ISA.x86, ISA.arm, ISA.ppc: + case ISA.mips32, ISA.nvptx, ISA.riscv32: + case ISA.sparc, ISA.s390, ISA.hppa: + case ISA.sh, ISA.webAssembly: opMode = [32]; break; - case ISA.ia64: - case ISA.alpha: + case ISA.ia64, ISA.alpha: opMode = [64]; break; case ISA.systemZ: @@ -214,20 +175,29 @@ ProcessorInfo getProcessorInfo() if (isRoot) { - auto dmi = execute("dmidecode -t 4", false).split("\n"); - r.voltage = dmi.getFromDmi("Voltage:").map!(a => a.split(" ")[0]).array.uniqIfSame[0]; - r.frequency = dmi.getFromDmi("Current Speed:") - .map!(a => a.split(" ")[0].to!size_t).array.uniqIfSame; - r.maxFrequency = dmi.getFromDmi("Max Speed:") - .map!(a => a.split(" ")[0].to!size_t).array.uniqIfSame; - r.cpus = dmi.getFromDmi("Processor Information").length; - r.cores = dmi.getFromDmi("Core Count").map!(a => a.to!size_t).array.uniqIfSame; - r.threads = dmi.getFromDmi("Thread Count").map!(a => a.to!size_t).array.uniqIfSame; + auto dmi = execute("dmidecode -t 4", false).splitLines; + r.voltage = dmi.parseDmiDataUniq("Voltage:", a => a.split(" ")[0])[0]; + r.frequency = dmi.parseDmiDataUniq("Current Speed:", a => a.split(" ")[0].to!size_t); + r.maxFrequency = dmi.parseDmiDataUniq("Max Speed:", a => a.split(" ")[0].to!size_t); + r.cpus = dmi.parseDmiData("Processor Information").length; + r.cores = dmi.parseDmiDataUniq!size_t("Core Count"); + r.threads = dmi.parseDmiDataUniq!size_t("Thread Count"); } return r; } +T[] parseDmiData(T = string)(string[] dmi, string key, T delegate(string) transform = a => a.to!T) +{ + return dmi.getFromDmi(key).map!(transform).array; +} + +T[] parseDmiDataUniq(T = string)(string[] dmi, string key, T delegate(string) transform = a => a + .to!T) +{ + return dmi.parseDmiData!T(key, transform).uniqIfSame; +} + struct MotherboardInfo { string vendor; @@ -308,21 +278,14 @@ string getDistribution() auto distribution = execute("uname -o", false); if (exists("/etc/os-release")) { - foreach (line; execute([ - "awk", "-F", "=", "/^NAME=/ {print $2}", "/etc/os-release" - ], false).split("\n")) - { - distribution = line; - } + distribution = execute([ + "awk", "-F", "=", "/^NAME=/ {print $2}", "/etc/os-release" + ], false).strip; } else if (distribution == "Darwin") - { distribution = execute("sw_vers", false); - } else if (exists("/etc/lsb-release")) - { distribution = execute("lsb_release -i", false); - } return distribution; } @@ -331,12 +294,9 @@ string getDistributionVersion() auto distributionVersion = execute("uname -r", false); if (exists("/etc/os-release")) { - foreach (line; execute([ - "awk", "-F", "=", "/^VERSION=/ {print $2}", "/etc/os-release" - ], false).split("\n")) - { - distributionVersion = line.strip("\""); - } + distributionVersion = execute([ + "awk", "-F", "=", "/^VERSION=/ {print $2}", "/etc/os-release" + ], false).strip("\""); } else if (execute("uname -o") == "Darwin") { @@ -344,9 +304,7 @@ string getDistributionVersion() "sw_vers -buildVersion", false) ~ " )"; } else if (exists("/etc/lsb-release")) - { distributionVersion = execute("lsb_release -r", false); - } return distributionVersion; } @@ -364,11 +322,15 @@ struct MemoryInfo string serial = "ROOT PERMISSIONS REQUIRED"; } +static immutable ExcludedStrings = [ + "Unknown", "No Module Installed", "Not Provided", "None" +]; + string[] getFromDmi(string[] dmi, string key) { return dmi.filter!(a => a.strip.startsWith(key)) - .map!(x => x.indexOf(":") != -1 ? x.split(":")[1].strip : x) - .filter!(a => a != "Unknown" && a != "No Module Installed" && a != "Not Provided" && a != "None") + .map!(x => x.canFind(":") ? x.split(":")[1].strip : x) + .filter!(a => ExcludedStrings.all!(s => a != s)) .array; } @@ -380,30 +342,30 @@ MemoryInfo getMemoryInfo() r.totalGB = r.total.split(" ")[0].to!int; if (isRoot) { - string[] dmi = execute("dmidecode -t memory", false).split("\n"); - r.type = dmi.getFromDmi("Type:").uniqIfSame.join("/"); - r.count = dmi.getFromDmi("Type:").length; - r.slots = dmi.getFromDmi("Memory Device") + string[] dmi = execute("dmidecode -t memory", false).splitLines; + r.type = dmi.parseDmiDataUniq("Type:").join("/"); + r.count = dmi.parseDmiData("Type:").length; + r.slots = dmi.parseDmiData("Memory Device") .filter!(a => a.indexOf("DMI type 17") != -1).array.length; - r.totalGB = dmi.getFromDmi("Size:").map!(a => a.split(" ")[0]) + r.totalGB = dmi.parseDmiData("Size:", a => a.split(" ")[0]) .array .filter!(isNumeric) .array .map!(to!int) .array .sum(); - r.total = r.totalGB.to!string ~ " GB (" ~ dmi.getFromDmi("Size:") - .map!(a => a.split(" ")[0]).join("/") ~ ")"; - auto totalWidth = dmi.getFromDmi("Total Width"); - auto dataWidth = dmi.getFromDmi("Data Width"); + r.total = r.totalGB.to!string ~ " GB (" ~ dmi.parseDmiData("Size:", a => a.split(" ")[0]).join( + "/") ~ ")"; + auto totalWidth = dmi.parseDmiData("Total Width"); + auto dataWidth = dmi.parseDmiData("Data Width"); foreach (i, width; totalWidth) { r.ecc ~= dataWidth[i] != width; } - r.speed = dmi.getFromDmi("Speed:").uniqIfSame.join("/"); - r.vendor = dmi.getFromDmi("Manufacturer:").uniqIfSame.join("/"); - r.partNumber = dmi.getFromDmi("Part Number:").uniqIfSame.join("/"); - r.serial = dmi.getFromDmi("Serial Number:").uniqIfSame.join("/"); + r.speed = dmi.parseDmiDataUniq("Speed:").join("/"); + r.vendor = dmi.parseDmiDataUniq("Manufacturer:").join("/"); + r.partNumber = dmi.parseDmiDataUniq("Part Number:").join("/"); + r.serial = dmi.parseDmiDataUniq("Serial Number:").join("/"); } @@ -453,41 +415,34 @@ StorageInfo getStorageInfo() foreach (JSONValue dev; lsblk["blockdevices"].array) { if (dev["id"].isNull) - { continue; - } Device d; - d.dev = dev["kname"].isNull ? "" : dev["kname"].str; - d.uuid = dev["id"].isNull ? "" : dev["id"].str; - d.type = dev["type"].isNull ? "" : dev["type"].str; - d.size = dev["size"].isNull ? "" : dev["size"].str; - d.model = dev["model"].isNull ? "Unknown Model" : dev["model"].str; - d.serial = dev["serial"].isNull ? "Missing Serial Number" : dev["serial"].str; - d.vendor = dev["vendor"].isNull ? "Unknown Vendor" : dev["vendor"].str; - d.state = dev["state"].isNull ? "" : dev["state"].str; - d.partitionTableType = dev["pttype"].isNull ? "" : dev["pttype"].str; - d.partitionTableUUID = dev["ptuuid"].isNull ? "" : dev["ptuuid"].str; - - switch (d.size[$ - 1]) - { - case 'B': - total += d.size[0 .. $ - 1].to!real; - break; - case 'K': - total += d.size[0 .. $ - 1].to!real * 1024; - break; - case 'M': - total += d.size[0 .. $ - 1].to!real * 1024 * 1024; - break; - case 'G': - total += d.size[0 .. $ - 1].to!real * 1024 * 1024 * 1024; - break; - case 'T': - total += d.size[0 .. $ - 1].to!real * 1024 * 1024 * 1024 * 1024; - break; - default: - assert(0, "Unknown size unit" ~ d.size[$ - 1]); - } + d.dev = getStrOrDefault(dev["kname"]); + d.uuid = getStrOrDefault(dev["id"]); + d.type = getStrOrDefault(dev["type"]); + d.size = getStrOrDefault(dev["size"]); + d.model = getStrOrDefault(dev["model"], "Unknown Model"); + d.serial = getStrOrDefault(dev["serial"], "Missing Serial Number"); + d.vendor = getStrOrDefault(dev["vendor"], "Unknown Vendor"); + d.state = getStrOrDefault(dev["state"]); + d.partitionTableType = getStrOrDefault(dev["pttype"]); + d.partitionTableUUID = getStrOrDefault(dev["ptuuid"]); + + int[char] sizeUnits = [ + 'B': 1, + 'K': 1024, + 'M': 1024 * 1024, + 'G': 1024 * 1024 * 1024, + 'T': 1024 * 1024 * 1024 * 1024 + ]; + + auto size = d.size[0 .. $ - 1].to!real; + auto unit = d.size[$ - 1]; + + if (unit in sizeUnits) + total += size * sizeUnits[unit]; + else + assert(0, "Unknown size unit: " ~ unit); if (isRoot) { @@ -496,17 +451,15 @@ StorageInfo getStorageInfo() foreach (JSONValue part; partData.array) { if (part["partuuid"].isNull) - { continue; - } Partition p; - p.dev = part["kname"].isNull ? "" : part["kname"].str; - p.fslabel = part["label"].isNull ? "" : part["label"].str; - p.partlabel = part["partlabel"].isNull ? "" : part["partlabel"].str; - p.size = part["size"].isNull ? "" : part["size"].str; - p.type = part["fstype"].isNull ? "" : part["fstype"].str; - p.mount = part["mountpoint"].isNull ? "Not Mounted" : part["mountpoint"].str; - p.id = part["partuuid"].isNull ? "" : part["partuuid"].str; + p.dev = getStrOrDefault(part["kname"]); + p.fslabel = getStrOrDefault(part["label"]); + p.partlabel = getStrOrDefault(part["partlabel"]); + p.size = getStrOrDefault(part["size"]); + p.type = getStrOrDefault(part["fstype"]); + p.mount = getStrOrDefault(part["mountpoint"], "Not Mounted"); + p.id = getStrOrDefault(part["partuuid"]); d.partitions ~= p; } } @@ -539,57 +492,57 @@ struct DisplayInfo size_t count; } +Display getDeviceData(JSONValue device) +{ + Display d; + + d.name = device["device_name"].str; + d.connected = device["is_connected"].boolean; + d.primary = device["is_primary"].boolean; + d.resolution = device["resolution_width"].integer.to!string ~ "x" ~ device["resolution_height"] + .integer.to!string; + foreach (JSONValue mode; device["modes"].array) + { + foreach (JSONValue freq; mode["frequencies"].array) + { + d.modes ~= mode["resolution_width"].integer.to!string ~ "x" ~ mode["resolution_height"].integer + .to!string ~ "@" ~ freq["frequency"].floating.to!string ~ "Hz"; + if (freq["is_current"].boolean) + d.refreshRate = freq["frequency"].floating.to!string ~ "Hz"; + } + } + if ("/sys/class/rm/card0-" ~ d.name.replace("HDMI-", "HDMI-A-") ~ "/edid".exists) + { + auto edidTmp = execute("edid-decode /sys/class/drm/card0-" ~ d.name.replace("HDMI-", "HDMI-A-") ~ "/edid", false); + auto edidData = getProcInfo(edidTmp, false); + d.vendor = ("Manufacturer" in edidData) ? edidData["Manufacturer"] : "Unknown"; + d.model = ("Model" in edidData) ? edidData["Model"] : "Unknown"; + d.serial = ("Serial Number" in edidData) ? edidData["Serial Number"] : "Unknown"; + d.manufactureDate = ("Made in" in edidData) ? edidData["Made in"] : "Unknown"; + d.size = ("Maximum image size" in edidData) ? edidData["Maximum image size"] : "Unknown"; + } + return d; +} + DisplayInfo getDisplayInfo() { DisplayInfo r; if ("DISPLAY" !in environment) return r; + JSONValue[] xrandr; try { - auto xrandr = execute!JSONValue("jc xrandr --properties", false)["screens"].array; - foreach (JSONValue screen; xrandr) - { - foreach (JSONValue device; screen["devices"].array) - { - Display d; - - d.name = device["device_name"].str; - d.connected = device["is_connected"].boolean; - d.primary = device["is_primary"].boolean; - d.resolution = device["resolution_width"].integer.to!string ~ "x" ~ device["resolution_height"] - .integer.to!string; - foreach (JSONValue mode; device["modes"].array) - { - foreach (JSONValue freq; mode["frequencies"].array) - { - d.modes ~= mode["resolution_width"].integer.to!string ~ "x" ~ mode["resolution_height"].integer - .to!string ~ "@" ~ freq["frequency"].floating.to!string ~ "Hz"; - if (freq["is_current"].boolean) - { - d.refreshRate = freq["frequency"].floating.to!string ~ "Hz"; - } - } - } - if ("/sys/class/rm/card0-" ~ d.name.replace("HDMI-", "HDMI-A-") ~ "/edid".exists) - { - auto edidTmp = execute("edid-decode /sys/class/drm/card0-" ~ d.name.replace("HDMI-", "HDMI-A-") ~ "/edid", false); - auto edidData = getProcInfo(edidTmp, false); - d.vendor = ("Manufacturer" in edidData) ? edidData["Manufacturer"] : "Unknown"; - d.model = ("Model" in edidData) ? edidData["Model"] : "Unknown"; - d.serial = ("Serial Number" in edidData) ? edidData["Serial Number"] : "Unknown"; - d.manufactureDate = ("Made in" in edidData) ? edidData["Made in"] : "Unknown"; - d.size = ("Maximum image size" in edidData) ? edidData["Maximum image size"] - : "Unknown"; - } - - r.displays ~= d; - r.count++; - } - } + xrandr = execute!JSONValue("jc xrandr --properties", false)["screens"].array; } catch (Exception e) - { return r; + foreach (JSONValue screen; xrandr) + { + foreach (JSONValue device; screen["devices"].array) + { + r.displays ~= getDeviceData(device); + r.count++; + } } return r; } @@ -602,25 +555,28 @@ struct GraphicsProcessorInfo string vram; } +string getGlxInfoValue(ref string[string] glxinfo, string key) +{ + return (key in glxinfo) ? glxinfo[key] : "Unknown"; +} + GraphicsProcessorInfo getGraphicsProcessorInfo() { GraphicsProcessorInfo r; if ("DISPLAY" !in environment) return r; + string[string] glxinfo; try { - auto glxinfo = getProcInfo(execute("glxinfo", false), false); - r.vendor = ("OpenGL vendor string" in glxinfo) ? glxinfo["OpenGL vendor string"] : "Unknown"; - r.model = ("OpenGL renderer string" in glxinfo) ? glxinfo["OpenGL renderer string"] - : "Unknown"; - r.coreProfile = ("OpenGL core profile version string" in glxinfo) ? glxinfo["OpenGL core profile version string"] : "Unknown"; - r.vram = ("Video memory" in glxinfo) ? glxinfo["Video memory"] : "Unknown"; + glxinfo = getProcInfo(execute("glxinfo", false), false); } catch (Exception e) - { return r; - } + r.vendor = getGlxInfoValue(glxinfo, "OpenGL vendor string"); + r.model = getGlxInfoValue(glxinfo, "OpenGL renderer string"); + r.coreProfile = getGlxInfoValue(glxinfo, "OpenGL core profile version string"); + r.vram = getGlxInfoValue(glxinfo, "Video memory"); return r; } @@ -649,9 +605,8 @@ struct MachineConfigInfo string[] videoDrivers; } -MachineConfigInfo getMachineConfigInfo() +void getPCIDeviceInfo(ref MachineConfigInfo info) { - MachineConfigInfo r; // PCI devices foreach (path; dirEntries("/sys/bus/pci/devices", SpanMode.shallow).map!(a => a.name).array) @@ -661,9 +616,7 @@ MachineConfigInfo getMachineConfigInfo() string _class = readText(path ~ "/class").strip; string _module; if (exists(path ~ "/driver/module")) - { _module = readLink(path ~ "/driver/module").baseName; - } if (_module != "" && ( // Mass-storage controller. Definitely important. @@ -674,7 +627,7 @@ MachineConfigInfo getMachineConfigInfo() // keyboard when things go wrong in the initrd. _class.startsWith("0x0c03"))) { - r.availableKernelModules ~= _module; + info.availableKernelModules ~= _module; } // broadcom STA driver (wl.ko) @@ -689,9 +642,9 @@ MachineConfigInfo getMachineConfigInfo() "0x4331", "0x43a0", "0x43b1" ].any!(a => device.startsWith(a))) { - r.extraModulePackages ~= Literal( + info.extraModulePackages ~= Literal( "config.boot.kernelPackages.broadcom_sta"); - r.kernelModules ~= "wl"; + info.kernelModules ~= "wl"; } // broadcom FullMac driver @@ -706,15 +659,13 @@ MachineConfigInfo getMachineConfigInfo() "0x43c5" ].any!(a => device.startsWith(a))) { - r.imports ~= Literal( + info.imports ~= Literal( "(modulesPath + \"/hardware/network/broadcom-43xx.nix\")"); } // In case this is a virtio scsi device, we need to explicitly make this available. if (vendor.startsWith("0x1af4") && ["0x1004", "0x1048"].any!(a => device.startsWith(a))) - { - r.availableKernelModules ~= "virtio_scsi"; - } + info.availableKernelModules ~= "virtio_scsi"; // Can't rely on $module here, since the module may not be loaded // due to missing firmware. Ideally we would check modules.pcimap @@ -724,12 +675,12 @@ MachineConfigInfo getMachineConfigInfo() if (["0x1043", "0x104f", "0x4220", "0x4221", "0x4223", "0x4224"].any!( a => device.startsWith(a))) { - r.literalAttrs ~= Literal( + info.literalAttrs ~= Literal( "networking.enableIntel2200BGFirmware = true;"); } else if (["0x4229", "0x4230", "0x4222", "0x4227"].any!(a => device.startsWith(a))) { - r.literalAttrs ~= Literal( + info.literalAttrs ~= Literal( "networking.enableIntel3945ABGFirmware = true;"); } } @@ -739,27 +690,26 @@ MachineConfigInfo getMachineConfigInfo() // FIXME: do we want to enable an unfree driver here? if (vendor.startsWith("0x10de") && _class.startsWith("0x03")) { - r.videoDrivers ~= "nvidia"; - r.blacklistedKernelModules ~= "nouveau"; + info.videoDrivers ~= "nvidia"; + info.blacklistedKernelModules ~= "nouveau"; } } +} +void getUSBDeviceInfo(ref MachineConfigInfo info) +{ // USB devices foreach (path; dirEntries("/sys/bus/usb/devices", SpanMode.shallow).map!(a => a.name).array) { if (!exists(path ~ "/bInterfaceClass")) - { continue; - } string _class = readText(path ~ "/bInterfaceClass").strip; string subClass = readText(path ~ "/bInterfaceSubClass").strip; string protocol = readText(path ~ "/bInterfaceProtocol").strip; string _module; if (exists(path ~ "/driver/module")) - { _module = readLink(path ~ "/driver/module").baseName; - } if (_module != "" && // Mass-storage controller. Definitely important. @@ -767,10 +717,13 @@ MachineConfigInfo getMachineConfigInfo() // Keyboard. Needed if we want to use the keyboard when things go wrong in the initrd. (subClass.startsWith("0x03") || protocol.startsWith("0x01"))) { - r.availableKernelModules ~= _module; + info.availableKernelModules ~= _module; } } +} +void getBlockDeviceInfo(ref MachineConfigInfo info) +{ // Block and MMC devices foreach (path; ( (exists("/sys/class/block") ? dirEntries("/sys/class/block", SpanMode.shallow) @@ -782,9 +735,13 @@ MachineConfigInfo getMachineConfigInfo() if (exists(path ~ "/device/driver/module")) { string _module = readLink(path ~ "/device/driver/module").baseName; - r.availableKernelModules ~= _module; + info.availableKernelModules ~= _module; } } +} + +void getBCacheInfo(ref MachineConfigInfo info) +{ // Bcache auto bcacheDevices = dirEntries("/dev", SpanMode.shallow).map!(a => a.name) .array @@ -793,40 +750,49 @@ MachineConfigInfo getMachineConfigInfo() bcacheDevices = bcacheDevices.filter!(device => device.indexOf("dev/bcachefs") == -1).array; if (bcacheDevices.length > 0) - { - r.availableKernelModules ~= "bcache"; - } + info.availableKernelModules ~= "bcache"; +} + +void getVMInfo(ref MachineConfigInfo info) +{ //Prevent unbootable systems if LVM snapshots are present at boot time. if (execute("lsblk -o TYPE", false).indexOf("lvm") != -1) - { - r.kernelModules ~= "dm-snapshot"; - } + info.kernelModules ~= "dm-snapshot"; // Check if we're in a VirtualBox guest. If so, enable the guest additions. auto virt = execute!ProcessPipes("systemd-detect-virt", false).stdout.readln.strip; switch (virt) { case "oracle": - r.literalAttrs ~= Literal("virtualisation.virtualbox.guest.enable = true;"); + info.literalAttrs ~= Literal("virtualisation.virtualbox.guest.enable = true;"); break; case "parallels": - r.literalAttrs ~= Literal("hardware.parallels.enable = true;"); - r.literalAttrs ~= Literal( + info.literalAttrs ~= Literal("hardware.parallels.enable = true;"); + info.literalAttrs ~= Literal( "nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ \"prl-tools\" ];"); break; - case "qemu": - case "kvm": - case "bochs": - r.imports ~= Literal("(modulesPath + \"/profiles/qemu-guest.nix\")"); + case "qemu", "kvm", "bochs": + info.imports ~= Literal("(modulesPath + \"/profiles/qemu-guest.nix\")"); break; case "microsoft": - r.literalAttrs ~= Literal("virtualization.hypervGuest.enable = true;"); + info.literalAttrs ~= Literal("virtualization.hypervGuest.enable = true;"); break; case "systemd-nspawn": - r.literalAttrs ~= Literal("boot.isContainer;"); + info.literalAttrs ~= Literal("boot.isContainer;"); break; default: break; } +} + +MachineConfigInfo getMachineConfigInfo() +{ + MachineConfigInfo r; + + r.getPCIDeviceInfo(); + r.getUSBDeviceInfo(); + r.getBlockDeviceInfo(); + r.getBCacheInfo(); + r.getVMInfo(); return r; } diff --git a/packages/mcl/src/src/mcl/commands/machine_create.d b/packages/mcl/src/src/mcl/commands/machine_create.d index ae6093b7..964cd60b 100755 --- a/packages/mcl/src/src/mcl/commands/machine_create.d +++ b/packages/mcl/src/src/mcl/commands/machine_create.d @@ -75,113 +75,116 @@ User getUser(string userName) return user; } +void writeFile(string filePath, string fileContent, string[] command = []) +{ + mkdirRecurse(filePath.dirName); + std.file.write(format("%s/%s", filePath), fileContent); + if (command.length > 0) + { + execute(command, false); + } +} + void createUserDir(User user) { - mkdirRecurse("users/" ~ user.userName); - string userNix = user.toNix; - std.file.write("users/" ~ user.userName ~ "/user-info.nix", userNix); - execute(["alejandra", "users/" ~ user.userName ~ "/user-info.nix"], false); - string gitConfig = generateGitConfig(user); - std.file.write("users/" ~ user.userName ~ "/.gitconfig", gitConfig); - - mkdirRecurse("users/" ~ user.userName ~ "/home-desktop"); - string homeDesktop = generateHomeDesktop(); - std.file.write("users/" ~ user.userName ~ "/home-desktop/default.nix", homeDesktop); - execute(["alejandra", "users/" ~ user.userName ~ "/home-desktop/default.nix"], false); - mkdirRecurse("users/" ~ user.userName ~ "/home-server"); - string homeServer = generateHomeServer(); - std.file.write("users/" ~ user.userName ~ "/home-server/default.nix", homeServer); - execute(["alejandra", "users/" ~ user.userName ~ "/home-server/default.nix"], false); + string userDir = format("users/%s/", user.userName); + string userInfoNix = format("%s/user-info.nix", userDir); + string userGitConfig = format("%s/.gitconfig", userDir); + writeFile(userInfoNix, user.toNix, ["alejandra", userInfoNix]); + writeFile(userGitConfig, generateGitConfig(user)); + + string homeDesktopDir = format("%s/home-desktop/", userDir); + string homeDesktopNix = format("%s/default.nix", homeDesktopDir); + writeFile(homeDesktopNix, generateHomeDesktop(), [ + "alejandra", homeDesktopNix + ]); + + string homeServerDir = format("%s/home-server/", userDir); + string homeServerNix = format("%s/default.nix", homeServerDir); + writeFile(homeServerNix, generateHomeServer(), ["alejandra", homeServerNix]); } string generateHomeServer() { - string homeServer = "{pkgs, ...}: {\n"; - homeServer ~= " home.packages = with pkgs; [\n"; - homeServer ~= " ];\n"; - homeServer ~= "}\n"; + string homeServer = "{pkgs, ...}: {\n + home.packages = with pkgs; [\n + ];\n + }\n"; return homeServer; } string generateHomeDesktop() { - string homeDesktop = "{pkgs, ...}: {\n"; - homeDesktop ~= " imports = [\n"; - homeDesktop ~= " ../home-server\n"; - homeDesktop ~= " ];\n"; - homeDesktop ~= " home.packages = with pkgs; [\n"; - homeDesktop ~= " ];\n"; - homeDesktop ~= "}\n"; + string homeDesktop = "{pkgs, ...}: {\n + imports = [\n + ../home-server\n + ];\n + home.packages = with pkgs; [\n + ];\n + }\n"; return homeDesktop; } string generateGitConfig(User user) { - string gitConfig = "[user]\n"; - gitConfig ~= " email = " ~ (user.emailInfo.workEmail != "" ? user.emailInfo.workEmail - : user.emailInfo.personalEmail) ~ "\n"; - gitConfig ~= " name = " ~ user.userInfo.description ~ "\n"; - gitConfig ~= "[fetch]\n"; - gitConfig ~= " prune = true\n"; - gitConfig ~= "[rebase]\n"; - gitConfig ~= " updateRefs = true\n"; - gitConfig ~= "[pull]\n"; - gitConfig ~= " ff = true\n"; - gitConfig ~= " rebase = false\n"; - gitConfig ~= "[merge]\n"; - gitConfig ~= " ff = only\n"; - gitConfig ~= "[core]\n"; - gitConfig ~= " editor = nvim\n"; - gitConfig ~= "[include]\n"; - gitConfig ~= " path = git/aliases.gitconfig\n"; - gitConfig ~= " path = git/delta.gitconfig\n"; - gitConfig ~= "[difftool \"diffpdf\"]\n"; - gitConfig ~= " cmd = diffpdf \\\"$LOCAL\\\" \\\"$REMOTE\\\"\n"; - gitConfig ~= "[difftool \"nvimdiff\"]\n"; - gitConfig ~= " cmd = nvim -d \\\"$LOCAL\\\" \\\"$REMOTE\\\"\n"; - gitConfig ~= "[diff]\n"; - gitConfig ~= " colorMoved = dimmed-zebra\n"; + string gitConfig = format("[user]\n + email = %s\n + name = %s\n + [fetch]\n + prune = true\n + [rebase]\n + updateRefs = true\n + [pull]\n + ff = true\n + rebase = false\n + [merge]\n + ff = only\n + [core]\n + editor = nvim\n + [include]\n + path = git/aliases.gitconfig\n + path = git/delta.gitconfig\n + [difftool \"diffpdf\"]\n + cmd = diffpdf \\\"$LOCAL\\\" \\\"$REMOTE\\\"\n + [difftool \"nvimdiff\"]\n + cmd = nvim -d \\\"$LOCAL\\\" \\\"$REMOTE\\\"\n + [diff]\n + colorMoved = dimmed-zebra\n", (user.emailInfo.workEmail != "" ? user.emailInfo.workEmail + : user.emailInfo.personalEmail), user.userInfo.description); return gitConfig; } void checkifNixosMachineConfigRepo() { + static immutable string repoUrl = "metacraft-labs/nixos-machine-config"; if (execute(["git", "config", "--get", "remote.origin.url"], false) - .indexOf("metacraft-labs/nixos-machine-config") == -1) + .indexOf(repoUrl) == -1) { - assert(0, "This is not the repo metacraft-labs/nixos-machine-config"); + assert(0, format("This is not the repo %s", repoUrl)); } } +string[] getGroupsFromFile(DirEntry input) +{ + string name = input.name ~ "/user-info.nix"; + if (!std.file.exists(name)) + return ["metacraft"]; + auto userInfoFile = nix.eval!JSONValue(name, ["--file"]); + if ("userInfo" !in userInfoFile || userInfoFile["userInfo"].isNull + || "extraGroups" !in userInfoFile["userInfo"] || userInfoFile["userInfo"]["extraGroups"] + .isNull) + return ["metacraft"]; + return userInfoFile["userInfo"]["extraGroups"].array.map!(a => a.str).array; +} + string[] getGroups() { - string[] groups = dirEntries("users", SpanMode.shallow).map!(a => a.name ~ "/user-info.nix") - .array - .map!((a) { - if (!std.file.exists(a)) - { - return JSONValue(["metacraft"]).array; - } - auto userInfoFile = nix.eval!JSONValue(a, ["--file"]); - if ("userInfo" !in userInfoFile || userInfoFile["userInfo"].isNull) - { - return JSONValue(["metacraft"]).array; - } - if ("extraGroups" !in userInfoFile["userInfo"] || userInfoFile["userInfo"]["extraGroups"] - .isNull) - { - return JSONValue(["metacraft"]).array; - } - return userInfoFile["userInfo"]["extraGroups"].array; - }) - .array + string[] groups = dirEntries("users", SpanMode.shallow) + .map!getGroupsFromFile .joiner .array - .map!(a => a.str) - .array .sort - .array .uniq .array; return groups; @@ -189,7 +192,6 @@ string[] getGroups() User createUser() { - auto createUser = params.createUser || prompt!bool("Create new user"); if (!createUser) { @@ -198,21 +200,19 @@ User createUser() : prompt!string("Select an existing username", existingUsers); return getUser(userName); } - else - { - User user; - user.userName = params.userName != "" ? params.userName - : prompt!string("Enter the new username"); - user.userInfo.description = params.description != "" ? params.description - : prompt!string("Enter the user's description/full name"); - user.userInfo.isNormalUser = params.isNormalUser || prompt!bool( - "Is this a normal or root user"); - user.userInfo.extraGroups = (params.extraGroups != "" ? params.extraGroups - : prompt!string("Enter the user's extra groups (comma delimited)", getGroups())).split(",") - .map!(strip).array; - createUserDir(user); - return user; - } + + User user; + user.userName = params.userName != "" ? params.userName + : prompt!string("Enter the new username"); + user.userInfo.description = params.description != "" ? params.description + : prompt!string("Enter the user's description/full name"); + user.userInfo.isNormalUser = params.isNormalUser || prompt!bool( + "Is this a normal or root user"); + user.userInfo.extraGroups = (params.extraGroups != "" ? params.extraGroups + : prompt!string("Enter the user's extra groups (comma delimited)", getGroups())).split(",") + .map!(strip).array; + createUserDir(user); + return user; } struct MachineConfiguration @@ -253,15 +253,8 @@ struct MachineConfiguration MachineUserInfo users; } -void createMachine(MachineType machineType, string machineName, User user) +MachineConfiguration getMachineConfiguration(User user, Info info) { - auto infoJSON = execute([ - "ssh", params.sshPath, - "sudo nix --experimental-features \\'nix-command flakes\\' --refresh --accept-flake-config run github:metacraft-labs/nixos-modules/feat/machine_create#mcl host_info" - ], false, false); - auto infoJSONParsed = infoJSON.parseJSON; - Info info = infoJSONParsed.fromJSON!Info; - MachineConfiguration machineConfiguration; machineConfiguration.users.users[user.userName] = MachineConfiguration .MachineUserInfo.UserData([user.userName] ~ "wheel"); @@ -269,12 +262,32 @@ void createMachine(MachineType machineType, string machineName, User user) machineConfiguration.networking.hostId = executeShell( "tr -dc 0-9a-f < /dev/urandom | head -c 8").output; machineConfiguration.mcl.host_info.sshKey = info.softwareInfo.opensshInfo.publicKey; + return machineConfiguration; +} + +void saveMachineConfiguration(MachineType machineType, string machineName, Info info, User user) +{ + MachineConfiguration machineConfiguration = getMachineConfiguration(user, info); string machineNix = machineConfiguration.toNix(["config", "dots"]) .replace("host_info", "host-info"); - mkdirRecurse("machines/" ~ machineType.to!string ~ "/" ~ machineName); - std.file.write("machines/" ~ machineType.to!string ~ "/" ~ machineName ~ "/" ~ "configuration.nix", machineNix); - // writeln(info.toJSON(true).toPrettyString()); + string filePath = format("machines/%s/%s/configuration.nix", machineType.to!string, machineName); + writeFile(filePath, machineNix, ["alejandra", filePath]); + +} + +Info getInfoOverSSH() +{ + auto infoJSON = execute([ + "ssh", params.sshPath, + "sudo nix --experimental-features \\'nix-command flakes\\' --refresh --accept-flake-config run github:metacraft-labs/nixos-modules/feat/machine_create#mcl host_info" + ], false, false); + auto infoJSONParsed = infoJSON.parseJSON; + Info info = infoJSONParsed.fromJSON!Info; + return info; +} +HardwareConfiguration initHardwareConfiguration(Info info) +{ HardwareConfiguration hardwareConfiguration; hardwareConfiguration.hardware.cpu["intel"] = HardwareConfiguration.Hardware.Cpu(); @@ -318,7 +331,11 @@ void createMachine(MachineType machineType, string machineName, User user) hardwareConfiguration.boot.initrd.availableKernelModules ~= [ "nvme", "xhci_pci", "usbhid", "usb_storage", "sd_mod" ]; + return hardwareConfiguration; +} +void initHardwareConfigurationDisko(HardwareConfiguration hardwareConfiguration, Info info) +{ // Disks hardwareConfiguration.disko.DISKO.makeZfsPartitions.swapSizeGB = ( info.hardwareInfo.memoryInfo.totalGB.to!double * 1.5).to!int; @@ -336,9 +353,10 @@ void createMachine(MachineType machineType, string machineName, User user) .map!(a => "/dev/disk/by-id/nvme-" ~ a) .array; hardwareConfiguration.disko.DISKO.makeZfsPartitions.disks = disks; +} - hardwareConfiguration = hardwareConfiguration.uniqArrays; - +void processHardwareConfigNix(HardwareConfiguration hardwareConfiguration, MachineType machineType, string machineName) +{ string hardwareNix = hardwareConfiguration.toNix([ "config", "lib", "pkgs", "modulesPath", "dirs", "dots" ]) @@ -346,15 +364,23 @@ void createMachine(MachineType machineType, string machineName, User user) .replace("makeZfsPartitions = ", "makeZfsPartitions ") .replace("SYSTEMDBOOT", "systemd-boot") .replace("mcl.host-info.sshKey", "# mcl.host-info.sshKey"); - std.file.write("machines/" ~ machineType.to!string ~ "/" ~ machineName ~ "/" ~ "hw-config.nix", hardwareNix); - execute([ - "alejandra", - "machines/" ~ machineType.to!string ~ "/" ~ machineName ~ "/" ~ "configuration.nix" - ], false); - execute([ - "alejandra", - "machines/" ~ machineType.to!string ~ "/" ~ machineName ~ "/" ~ "hw-config..nix" - ], false); + string filePath = format("machines/%s/%s/hw-config.nix", machineType.to!string, machineName); + writeFile(filePath, hardwareNix, ["alejandra", filePath]); +} + +void createMachine(MachineType machineType, string machineName, User user) +{ + Info info = getInfoOverSSH(); + + saveMachineConfiguration(machineType, machineName, info, user); + + HardwareConfiguration hardwareConfiguration = initHardwareConfiguration(info); + + initHardwareConfigurationDisko(hardwareConfiguration, info); + + hardwareConfiguration = hardwareConfiguration.uniqArrays; + + processHardwareConfigNix(hardwareConfiguration, machineType, machineName); } struct HardwareConfiguration diff --git a/packages/mcl/src/src/mcl/commands/shard_matrix.d b/packages/mcl/src/src/mcl/commands/shard_matrix.d index d6d79e14..675cde3e 100644 --- a/packages/mcl/src/src/mcl/commands/shard_matrix.d +++ b/packages/mcl/src/src/mcl/commands/shard_matrix.d @@ -148,9 +148,7 @@ void saveShardMatrix(ShardMatrix matrix, Params params) infof("Shard matrix: %s", matrixJson.toPrettyString); const envLine = "gen_matrix=" ~ matrixString; if (params.githubOutput != "") - { params.githubOutput.append(envLine); - } else { createResultDirs(); diff --git a/packages/mcl/src/src/mcl/utils/array.d b/packages/mcl/src/src/mcl/utils/array.d index e1a4b2ff..ed7bc833 100644 --- a/packages/mcl/src/src/mcl/utils/array.d +++ b/packages/mcl/src/src/mcl/utils/array.d @@ -8,17 +8,11 @@ import std.stdio : writeln; T[] uniqIfSame(T)(T[] arr) { if (arr.length == 0) - { return arr; - } else if (arr.all!(a => a == arr[0])) - { return [arr[0]]; - } else - { return arr; - } } @@ -33,26 +27,13 @@ unittest T uniqArrays(T)(T s) { - static if (isSomeString!T) - { - return s; - } - else static if (isArray!T) - { - return s.sort.uniq.array.to!T; - } + static if (isArray!T && !isSomeString!T) + s = s.sort.uniq.array.to!T; else static if (is(T == struct)) - { static foreach (idx, field; T.tupleof) - { s.tupleof[idx] = s.tupleof[idx].uniqArrays; - } - return s; - } - else - { - return s; - } + + return s; } @("uniqArrays") diff --git a/packages/mcl/src/src/mcl/utils/coda.d b/packages/mcl/src/src/mcl/utils/coda.d index a2d87354..d098c267 100644 --- a/packages/mcl/src/src/mcl/utils/coda.d +++ b/packages/mcl/src/src/mcl/utils/coda.d @@ -681,40 +681,32 @@ struct CodaApiClient http.addRequestHeader("Content-Type", "application/json"); http.addRequestHeader("Authorization", "Bearer " ~ this.apiToken); + JSONValue ret = parseJSON("{}"); + auto reqBody = req.toString(JSONOptions.doNotEscapeSlashes); + reqBody = (reqBody == "null") ? "" : reqBody; + static if (method == HTTP.Method.get) { auto resp = httpGet(baseEndpoint ~ endpoint, http); - return parseJSON(resp); + ret = parseJSON(resp); } else static if (method == HTTP.Method.post) { - auto reqBody = req.toString(JSONOptions.doNotEscapeSlashes); - reqBody = (reqBody == "null") ? "" : reqBody; auto resp = httpPost(baseEndpoint ~ endpoint, reqBody, http); - return parseJSON(resp); + ret = parseJSON(resp); } else static if (method == HTTP.Method.put) { - auto reqBody = req.toString(JSONOptions.doNotEscapeSlashes); - reqBody = (reqBody == "null") ? "" : reqBody; auto resp = httpPut(baseEndpoint ~ endpoint, reqBody, http); - return parseJSON(resp); + ret = parseJSON(resp); } else static if (method == HTTP.Method.del) - { - auto reqBody = req.toString(JSONOptions.doNotEscapeSlashes); - reqBody = (reqBody == "null") ? "" : reqBody; httpDelete(baseEndpoint ~ endpoint, http); - return parseJSON("{}"); - } else static if (method == HTTP.Method.patch) - { - auto reqBody = req.toString(JSONOptions.doNotEscapeSlashes); - reqBody = (reqBody == "null") ? "" : reqBody; httpPatch(baseEndpoint ~ endpoint, reqBody, http); - return parseJSON("{}"); - } else static assert(0, "Please implement " ~ method); + + return ret; } } diff --git a/packages/mcl/src/src/mcl/utils/json.d b/packages/mcl/src/src/mcl/utils/json.d index 774c3455..178eb82d 100644 --- a/packages/mcl/src/src/mcl/utils/json.d +++ b/packages/mcl/src/src/mcl/utils/json.d @@ -13,6 +13,16 @@ import std.datetime : SysTime; import std.sumtype : SumType, isSumType; import core.stdc.string : strlen; +string getStrOrDefault(JSONValue value, string defaultValue = "") +{ + return value.isNull ? defaultValue : value.str; +} + +string jsonValueToString(in JSONValue value) +{ + return value.toString(JSONOptions.doNotEscapeSlashes).strip("\""); +} + bool tryDeserializeJson(T)(in JSONValue value, out T result) { try @@ -28,57 +38,50 @@ bool tryDeserializeJson(T)(in JSONValue value, out T result) T fromJSON(T)(in JSONValue value) { + T result; if (value.isNull) - { - return T.init; - } + result = T.init; + static if (is(T == JSONValue)) - { - return value; - } + result = value; else static if (is(T == bool) || is(T == string) || isSomeChar!T || isNumeric!T || is(T == enum)) - { - return value.toString(JSONOptions.doNotEscapeSlashes).strip("\"").to!T; - } + result = jsonValueToString(value).to!T; else static if (isSumType!T) { + bool sumTypeDecoded = false; static foreach (SumTypeVariant; T.Types) { { - SumTypeVariant result; - if (tryDeserializeJson!SumTypeVariant(value, result)) + SumTypeVariant sumTypeResult; + if (tryDeserializeJson!SumTypeVariant(value, sumTypeResult)) { - return T(result); + sumTypeDecoded = true; + result = sumTypeResult; } } } - - throw new Exception("Failed to deserialize JSON value"); + if (!sumTypeDecoded) + throw new Exception("Failed to deserialize JSON value"); } else static if (isArray!T) { static if (isBoolean!(ForeachType!T)) { if (value.type == JSONType.string && isBoolean!(ForeachType!T)) - { - return value.str.map!(a => a == '1').array; - } + result = value.str.map!(a => a == '1').array; } - if (value.type != JSONType.array) - { - return [value.fromJSON!(ForeachType!T)]; - } + result = [value.fromJSON!(ForeachType!T)]; + else + result = value.array.map!(a => a.fromJSON!(ForeachType!T)).array; - return value.array.map!(a => a.fromJSON!(ForeachType!T)).array; } else static if (is(T == SysTime)) { - return SysTime.fromISOExtString(value.toString(JSONOptions.doNotEscapeSlashes).strip("\"")); + result = SysTime.fromISOExtString(jsonValueToString(value)); } else static if (is(T == struct)) { - T result; static foreach (idx, field; T.tupleof) { if ((__traits(identifier, field).replace("_", "") in value.object) && !value[__traits(identifier, field) @@ -88,24 +91,18 @@ T fromJSON(T)(in JSONValue value) .replace("_", "")].fromJSON!(typeof(field)); } } - return result; } else static if (isAssociativeArray!T) { - T result; foreach (key, val; value.object) { if (key in result) - { result[key] = val.fromJSON!(typeof(result[key])); - } } - return result; } else - { static assert(false, "Unsupported type: `", T, "` ", isSumType!T); - } + return result; } @@ -118,56 +115,50 @@ unittest JSONValue toJSON(T)(in T value, bool simplify = false) { + JSONValue result; static if (is(T == enum)) - { - return JSONValue(value.enumToString); - } + result = JSONValue(value.enumToString); else static if (is(T == bool) || is(T == string) || isSomeChar!T || isNumeric!T) - return JSONValue(value); + result = JSONValue(value); else static if ((isArray!T && isSomeChar!(ForeachType!T))) - { - return JSONValue(value.idup[0 .. (strlen(value.ptr) - 1)]); - } + result = JSONValue(value.idup[0 .. (strlen(value.ptr) - 1)]); else static if (isArray!T) { if (simplify && value.length == 1) - return value.front.toJSON(simplify); + result = value.front.toJSON(simplify); else if (simplify && isBoolean!(ForeachType!T)) { static if (isBoolean!(ForeachType!T)) - { - return JSONValue((value.map!(a => a ? '1' : '0').array).to!string); - } + result = JSONValue((value.map!(a => a ? '1' : '0').array).to!string); else - { assert(0); - } } else { - JSONValue[] result; + JSONValue[] arrayResult; foreach (elem; value) - result ~= elem.toJSON(simplify); - return JSONValue(result); + arrayResult ~= elem.toJSON(simplify); + result = JSONValue(arrayResult); } } else static if (is(T == SysTime)) - { - return JSONValue(value.toISOExtString()); - } + result = JSONValue(value.toISOExtString()); else static if (is(T == struct)) { - JSONValue[string] result; + JSONValue[string] structResult; auto name = ""; static foreach (idx, field; T.tupleof) { name = __traits(identifier, field).strip("_"); - result[name] = value.tupleof[idx].toJSON(simplify); + structResult[name] = value.tupleof[idx].toJSON(simplify); } - return JSONValue(result); + result = JSONValue(structResult); } else static assert(false, "Unsupported type: `" ~ __traits(identifier, T) ~ "`"); + + return result; + } version (unittest) diff --git a/packages/mcl/src/src/mcl/utils/path.d b/packages/mcl/src/src/mcl/utils/path.d index 89b5bb4e..1c3c6e49 100644 --- a/packages/mcl/src/src/mcl/utils/path.d +++ b/packages/mcl/src/src/mcl/utils/path.d @@ -47,10 +47,7 @@ unittest assert(gcRootsDir == resultDir.buildNormalizedPath("gc-roots")); } -void createResultDirs() -{ - mkdirRecurse(gcRootsDir); -} +void createResultDirs() => mkdirRecurse(gcRootsDir); @("createResultDirs") unittest diff --git a/packages/mcl/test.sh b/packages/mcl/test.sh index 3cba1c5c..2781f23a 100755 --- a/packages/mcl/test.sh +++ b/packages/mcl/test.sh @@ -1,4 +1,4 @@ #!/usr/bin/env sh #export LD_DEBUG=all export LD_LIBRARY_PATH=$(nix eval --raw nixpkgs#curl.out.outPath)/lib:$LD_LIBRARY_PATH -dub test -- -e 'coda|fetchJson' +dub test --build="unittest" --compiler ldc2 -- -e 'coda|fetchJson'