From 4ae18424385e13f93f4bad7e8fa1a55fb76749e8 Mon Sep 17 00:00:00 2001 From: Matheus Catarino Date: Tue, 12 May 2026 17:24:53 -0300 Subject: [PATCH 1/3] CLI fixes --- dub.json | 2 +- source/app.d | 25 ++++++++++++++++-------- source/impl.d | 53 +++++++++++++++++++++++++++++++++++---------------- 3 files changed, 55 insertions(+), 25 deletions(-) diff --git a/dub.json b/dub.json index 9a55555..f0713ef 100644 --- a/dub.json +++ b/dub.json @@ -1,6 +1,6 @@ { "name": "ldcup", - "version": "1.1.7", + "version": "1.1.8", "description": "Download and manage D compiler [ldc2].", "license": "Apache-2.0", "targetPath": "bin", diff --git a/source/app.d b/source/app.d index 5f3db56..06d92bd 100644 --- a/source/app.d +++ b/source/app.d @@ -21,6 +21,7 @@ struct Args bool help; bool verbose; bool remote; + bool compilerSet; string installDir; string platform; string command; @@ -36,7 +37,7 @@ void printHelp(string programName) @safe writeln(" install [compiler] Install a compiler (default: ldc2-latest)"); writeln(" uninstall Uninstall an installed compiler"); writeln(" list List installed compilers"); - writeln(" run -- Run ldc2 with the given flags"); + writeln(" run [compiler] -- Run ldc2 with the given flags"); writeln(); writeln("Compiler specifiers:"); writeln(" ldc2-latest Latest stable LDC2 release (default)"); @@ -88,9 +89,22 @@ Args parseArgs(string[] argv) @safe break; } else if (arg.startsWith("ldc2-") || arg.startsWith("opend-")) + { parsed.compiler = normaliseCompilerSpec(arg); - else if (arg.canFind("redub")) + parsed.compilerSet = true; + } + else if (arg == "redub") + { parsed.compiler = arg; + parsed.compilerSet = true; + } + else if (arg.among("dmd", "gdc")) + throw new Exception("Only ldc2 and opend compilers are supported."); + else if (arg.startsWith("v") && arg.length > 1 && arg[1].isDigit) + { + parsed.compiler = normaliseCompilerSpec(arg); + parsed.compilerSet = true; + } else if (parsed.command.empty) parsed.command = arg; else @@ -118,12 +132,6 @@ int main(string[] argv) @safe return 0; } - if (parsed.compiler.among("dmd", "gdc")) - { - writeln("Error: only ldc2 and opend compilers are supported."); - return 1; - } - if (parsed.command.empty) { writeln("Error: no command specified."); @@ -143,6 +151,7 @@ int main(string[] argv) @safe break; case "uninstall": + enforce(parsed.compilerSet, "uninstall requires a compiler specifier, e.g. ldc2-1.42.0"); manager.uninstallCompiler(parsed.compiler); break; diff --git a/source/impl.d b/source/impl.d index 2297095..321b9b7 100644 --- a/source/impl.d +++ b/source/impl.d @@ -63,10 +63,10 @@ private: Arch currentArch; ReleaseType releaseType; - version (Windows) - immutable string ext = ".7z"; - else - immutable string ext = ".tar.xz"; + private string ext() const @safe pure + { + return (currentOS == OS.windows) ? ".7z" : ".tar.xz"; + } public: bool verbose; @@ -137,6 +137,9 @@ public: enforce(parts.length == 2, "Invalid platform format (expected OS-ARCH): " ~ platform); currentOS = fromString!OS(parts[0]); currentArch = fromString!Arch(parts[1]); + // Windows releases use x64/x86/multilib naming, not x86_64 + if (currentOS == OS.windows && currentArch == Arch.x86_64) + currentArch = Arch.x64; } } @@ -150,9 +153,9 @@ public: auto downloadUrl = getCompilerDownloadUrl(resolvedCompiler); auto targetPath = buildPath(root, resolvedCompiler); - downloadAndExtract(downloadUrl, targetPath, resolvedCompiler.startsWith("opend-")); + bool isOpend = resolvedCompiler.startsWith("opend-"); + downloadAndExtract(downloadUrl, targetPath, isOpend); - bool isOpend = compilerSpec.startsWith("opend-"); string prefix = isOpend ? "opend" : "ldc2"; compilerPath = buildPath( targetPath, @@ -198,10 +201,9 @@ public: void runCompiler(string compilerSpec, string[] args) @safe { - enforce(args.length > 0, "No flags provided. Use 'run -- '"); + enforce(args.length > 0, "No flags provided. Use 'run [compiler] -- '"); log("Running compiler: %s", compilerSpec); - compilerPath = findLDC2Path(); - enforce(!compilerPath.empty, "No LDC2 installation found"); + compilerPath = findLDC2Path(compilerSpec); auto cmd = [compilerPath] ~ args; auto result = execute(cmd); @@ -209,14 +211,18 @@ public: enforce(result.status == 0, "LDC2 execution failed with status %d".format(result.status)); } - string findLDC2Path() @safe + string findLDC2Path(string compilerSpec = "") @safe { auto installed = listInstalledCompilers().filter!(v => v.startsWith("ldc2-")).array; enforce(!installed.empty, "No LDC2 installation found in %s".format(root)); - string ver = installed[0]["ldc2-".length .. $]; + // Use the requested spec if it's installed; otherwise fall back to the first entry. + string entry = (compilerSpec.startsWith("ldc2-") && installed.canFind(compilerSpec)) + ? compilerSpec : installed[0]; + + string ver = entry["ldc2-".length .. $]; string ldc2Dir = buildPath( - root, installed[0], + root, entry, format("ldc2-%s-%s-%s", ver, currentOS, currentArch), "bin" ); @@ -467,7 +473,7 @@ public: else if (compilerSpec.endsWith("nightly") || compilerSpec.endsWith("master")) { releaseType = ReleaseType.nightly; - url = "https://github.com/ldc-developers/ldc/commits/master.atom"; + url = "https://api.github.com/repos/ldc-developers/ldc/releases/tags/CI"; } else // Explicit version — use as-is; normalise prefix if missing. return compilerSpec.startsWith("ldc2-") ? compilerSpec : "ldc2-" ~ compilerSpec; @@ -485,9 +491,24 @@ public: format("HTTP %d when resolving %s version", res.code, releaseType)); string response = (cast(string) res.responseBody.data).strip; - string dversion = (releaseType == ReleaseType.nightly) - ? response.split("tag:github.com,2008:Grit::Commit/")[1].split( - "")[0][0 .. 8] : response; + string dversion; + if (releaseType == ReleaseType.nightly) + { + string suffix = format("-%s-%s%s", currentOS, currentArch, ext); + foreach (asset; parseJSON(response)["assets"].array) + { + string name = asset["name"].str; + if (name.startsWith("ldc2-") && name.endsWith(suffix)) + { + dversion = name["ldc2-".length .. $ - suffix.length]; + break; + } + } + enforce(!dversion.empty, + "No CI nightly build found for %s-%s".format(currentOS, currentArch)); + } + else + dversion = response; log("Resolved %s version: ldc2-%s", releaseType, dversion); return "ldc2-" ~ dversion; From a0358b8c1427eb42e011934f50612f75df24617a Mon Sep 17 00:00:00 2001 From: Matheus Catarino Date: Tue, 12 May 2026 17:39:42 -0300 Subject: [PATCH 2/3] add unittests --- .github/workflows/ci.yml | 64 ++++++++-------- source/app.d | 113 +--------------------------- source/impl.d | 156 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 192 insertions(+), 141 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5595782..c87fe77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,14 +30,14 @@ jobs: fail-fast: false runs-on: ${{matrix.os}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - uses: dlang-community/setup-dlang@v2 with: compiler: ldc-latest - uses: ilammy/msvc-dev-cmd@v1 if: runner.os == 'Windows' with: - arch: ${{ matrix.arch }} + arch: ${{ matrix.arch }} - name: Set CURL_CA_BUNDLE for Windows if: runner.os == 'Windows' shell: pwsh @@ -50,17 +50,20 @@ jobs: run: | echo "DUB_ARCH=--arch=arm64-windows-msvc" >> $GITHUB_ENV echo "CC=cl.exe" >> $GITHUB_ENV - echo "CXX=cl.exe" >> $GITHUB_ENV + echo "CXX=cl.exe" >> $GITHUB_ENV - name: Build - run: | - dub -b release ${{ env.DUB_ARCH }} - - name: Tests + run: dub -b release ${{ env.DUB_ARCH }} + - name: Unit tests + run: dub test ${{ env.DUB_ARCH }} + - name: Integration tests + shell: bash run: | dub -b release ${{ env.DUB_ARCH }} -- install -v + LATEST=$(dub -b release ${{ env.DUB_ARCH }} -- list | head -1) dub -b release ${{ env.DUB_ARCH }} -- install ldc2-master -v - dub -b release ${{ env.DUB_ARCH }} -- list -v - dub -b release ${{ env.DUB_ARCH }} -- uninstall ldc2-1.42.0 -v - dub -b release ${{ env.DUB_ARCH }} -- list -v + dub -b release ${{ env.DUB_ARCH }} -- list + dub -b release ${{ env.DUB_ARCH }} -- uninstall "$LATEST" -v + dub -b release ${{ env.DUB_ARCH }} -- list dub -b release ${{ env.DUB_ARCH }} -- run -v -- --version - name: Compress artifacts (Windows) if: runner.os == 'Windows' @@ -70,7 +73,7 @@ jobs: if: runner.os != 'Windows' run: | cd bin && tar -cJf ldcup-${{ matrix.os }}-${{ matrix.arch }}.tar.xz * - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@v4 if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') with: name: ldcup-${{ matrix.os }}-${{ matrix.arch }} @@ -89,7 +92,7 @@ jobs: fail-fast: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - name: Run FreeBSD VM uses: vmactions/freebsd-vm@v1 with: @@ -98,14 +101,16 @@ jobs: pkg install -y curl dub ldc run: | dub -b release + dub test dub -b release -- install -v + LATEST=$(dub -b release -- list | head -1) dub -b release -- install ldc2-master -v - dub -b release -- list -v - dub -b release -- uninstall ldc2-1.42.0 -v - dub -b release -- list -v + dub -b release -- list + dub -b release -- uninstall "$LATEST" -v + dub -b release -- list dub -b release -- run -v -- --version tar -cJf bin/ldcup-freebsd14.3-${{ matrix.arch }}.tar.xz -C bin . - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@v4 if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') with: name: ldcup-freebsd-14.3-amd64 @@ -129,27 +134,28 @@ jobs: run: shell: sh steps: - - uses: actions/checkout@v6 - + - uses: actions/checkout@v4 - name: Prepare run: | - apk update - apk add --no-cache xz tar ldc dub clang + apk update + apk add --no-cache xz tar ldc dub clang - name: Build run: | - dub -b release - touch $HOME/.profile - - name: Tests + dub -b release + touch $HOME/.profile + - name: Unit tests + run: dub test + - name: Integration tests run: | - dub -b release -- install ldc2-master -v - dub -b release -- list -v - source $HOME/.profile - dub -b release -- install redub -v - dub -b release -- run -v -- --version + dub -b release -- install ldc2-master -v + dub -b release -- list + source $HOME/.profile + dub -b release -- install redub -v + dub -b release -- run -v -- --version - name: Compress artifacts run: | - cd bin && tar -cJf ldcup-alpine-${{ matrix.arch }}.tar.xz * - - uses: actions/upload-artifact@v7 + cd bin && tar -cJf ldcup-alpine-${{ matrix.arch }}.tar.xz * + - uses: actions/upload-artifact@v4 if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') with: name: ldcup-alpine-${{ matrix.arch }} diff --git a/source/app.d b/source/app.d index 06d92bd..7763712 100644 --- a/source/app.d +++ b/source/app.d @@ -1,119 +1,8 @@ module app; import impl; -import std.stdio : writeln, writefln; -import std.string : format, startsWith, endsWith, toLower, split; -import std.algorithm : canFind, countUntil; -import std.exception : enforce; -import std.meta : AliasSeq; - -// std.algorithm.comparison.among requires an import; use a helper instead. -private bool among(T, Args...)(T val, Args choices) -{ - foreach (c; choices) - if (val == c) - return true; - return false; -} - -struct Args -{ - bool help; - bool verbose; - bool remote; - bool compilerSet; - string installDir; - string platform; - string command; - string compiler = "ldc2-latest"; - string[] compilerArgs; -} - -void printHelp(string programName) @safe -{ - writefln("Usage: %s [compiler] [options]", programName); - writeln(); - writeln("Commands:"); - writeln(" install [compiler] Install a compiler (default: ldc2-latest)"); - writeln(" uninstall Uninstall an installed compiler"); - writeln(" list List installed compilers"); - writeln(" run [compiler] -- Run ldc2 with the given flags"); - writeln(); - writeln("Compiler specifiers:"); - writeln(" ldc2-latest Latest stable LDC2 release (default)"); - writeln(" ldc2-beta Latest beta LDC2 release"); - writeln(" ldc2-nightly Latest nightly/CI build"); - writeln(" ldc2- Specific version, e.g. ldc2-1.39.0"); - writeln(" opend-latest Latest OpenD release"); - writeln(" redub Install the redub build tool"); - writeln(); - writeln("Options:"); - writeln(" --install-dir=DIR Override installation directory"); - writeln(" --platform=OS-ARCH Override platform (e.g. linux-x86_64)"); - writeln(" --remote (list) Show all available remote releases"); - writeln(" --verbose, -v Enable verbose output"); - writeln(" --help, -h Show this help message"); -} - -/// Strip a leading "v" from a version tag so "v1.39.0" → "ldc2-1.39.0". -private string normaliseCompilerSpec(string spec) @safe pure -{ - // e.g. user typed "v1.39.0" — treat as "ldc2-1.39.0" - if (spec.startsWith("v") && !spec.startsWith("verbose")) - return "ldc2-" ~ spec[1 .. $]; - return spec; -} - -Args parseArgs(string[] argv) @safe -{ - Args parsed; - - for (size_t i = 1; i < argv.length; ++i) - { - string arg = argv[i]; - - if (arg == "--help" || arg == "-h") - parsed.help = true; - else if (arg == "--verbose" || arg == "-v") - parsed.verbose = true; - else if (arg == "--remote") - parsed.remote = true; - else if (arg.startsWith("--platform=")) - parsed.platform = arg["--platform=".length .. $]; - else if (arg.startsWith("--install-dir=")) - parsed.installDir = arg["--install-dir=".length .. $]; - else if (arg == "--") - { - // Everything after "--" is passed verbatim to the compiler. - parsed.compilerArgs = argv[i + 1 .. $]; - break; - } - else if (arg.startsWith("ldc2-") || arg.startsWith("opend-")) - { - parsed.compiler = normaliseCompilerSpec(arg); - parsed.compilerSet = true; - } - else if (arg == "redub") - { - parsed.compiler = arg; - parsed.compilerSet = true; - } - else if (arg.among("dmd", "gdc")) - throw new Exception("Only ldc2 and opend compilers are supported."); - else if (arg.startsWith("v") && arg.length > 1 && arg[1].isDigit) - { - parsed.compiler = normaliseCompilerSpec(arg); - parsed.compilerSet = true; - } - else if (parsed.command.empty) - parsed.command = arg; - else - throw new Exception("Unknown argument: " ~ arg); - } - - return parsed; -} +version(unittest) {} else int main(string[] argv) @safe { Args parsed; diff --git a/source/impl.d b/source/impl.d index 321b9b7..9ed3b29 100644 --- a/source/impl.d +++ b/source/impl.d @@ -52,6 +52,109 @@ private string stripLeadingV(string s) @safe pure return (s.length > 0 && s[0] == 'v') ? s[1 .. $] : s; } +private bool among(T, Choices...)(T val, Choices choices) +{ + foreach (c; choices) + if (val == c) + return true; + return false; +} + +struct Args +{ + bool help; + bool verbose; + bool remote; + bool compilerSet; + string installDir; + string platform; + string command; + string compiler = "ldc2-latest"; + string[] compilerArgs; +} + +void printHelp(string programName) @safe +{ + writefln("Usage: %s [compiler] [options]", programName); + writeln(); + writeln("Commands:"); + writeln(" install [compiler] Install a compiler (default: ldc2-latest)"); + writeln(" uninstall Uninstall an installed compiler"); + writeln(" list List installed compilers"); + writeln(" run [compiler] -- Run ldc2 with the given flags"); + writeln(); + writeln("Compiler specifiers:"); + writeln(" ldc2-latest Latest stable LDC2 release (default)"); + writeln(" ldc2-beta Latest beta LDC2 release"); + writeln(" ldc2-nightly Latest nightly/CI build"); + writeln(" ldc2- Specific version, e.g. ldc2-1.39.0"); + writeln(" opend-latest Latest OpenD release"); + writeln(" redub Install the redub build tool"); + writeln(); + writeln("Options:"); + writeln(" --install-dir=DIR Override installation directory"); + writeln(" --platform=OS-ARCH Override platform (e.g. linux-x86_64)"); + writeln(" --remote (list) Show all available remote releases"); + writeln(" --verbose, -v Enable verbose output"); + writeln(" --help, -h Show this help message"); +} + +private string normaliseCompilerSpec(string spec) @safe pure +{ + if (spec.startsWith("v") && !spec.startsWith("verbose")) + return "ldc2-" ~ spec[1 .. $]; + return spec; +} + +Args parseArgs(string[] argv) @safe +{ + Args parsed; + + for (size_t i = 1; i < argv.length; ++i) + { + string arg = argv[i]; + + if (arg == "--help" || arg == "-h") + parsed.help = true; + else if (arg == "--verbose" || arg == "-v") + parsed.verbose = true; + else if (arg == "--remote") + parsed.remote = true; + else if (arg.startsWith("--platform=")) + parsed.platform = arg["--platform=".length .. $]; + else if (arg.startsWith("--install-dir=")) + parsed.installDir = arg["--install-dir=".length .. $]; + else if (arg == "--") + { + parsed.compilerArgs = argv[i + 1 .. $]; + break; + } + else if (arg.startsWith("ldc2-") || arg.startsWith("opend-")) + { + parsed.compiler = normaliseCompilerSpec(arg); + parsed.compilerSet = true; + } + else if (arg == "redub") + { + parsed.compiler = arg; + parsed.compilerSet = true; + } + else if (arg.among("dmd", "gdc")) + throw new Exception("Only ldc2 and opend compilers are supported."); + else if (arg.startsWith("v") && arg.length > 1 && arg[1].isDigit) + { + parsed.compiler = normaliseCompilerSpec(arg); + parsed.compilerSet = true; + } + else if (parsed.command.empty) + parsed.command = arg; + else + throw new Exception("Unknown argument: " ~ arg); + } + + return parsed; +} + class CompilerManager { private: @@ -724,3 +827,56 @@ string findProgram(string programName) @safe } throw new Exception("Program not found in PATH: " ~ programName); } + +// ─── Unit tests ──────────────────────────────────────────────────────────── + +unittest // ext property: runtime value based on currentOS, not compile-time host +{ + auto win = new CompilerManager(tempDir(), "windows-x64"); + assert(win.ext == ".7z", "Windows target must use .7z"); + + auto lin = new CompilerManager(tempDir(), "linux-x86_64"); + assert(lin.ext == ".tar.xz", "Linux target must use .tar.xz"); + + auto osx = new CompilerManager(tempDir(), "osx-universal"); + assert(osx.ext == ".tar.xz", "macOS target must use .tar.xz"); +} + +unittest // parseArgs: v-prefix version normalized to ldc2- +{ + + auto a = parseArgs(["ldcup", "install", "v1.42.0"]); + assert(a.compiler == "ldc2-1.42.0", "v1.42.0 must become ldc2-1.42.0"); + assert(a.compilerSet, "compilerSet must be true for explicit version"); +} + +unittest // parseArgs: dmd and gdc rejected with exception +{ + + import std.exception : assertThrown; + assertThrown(parseArgs(["ldcup", "install", "dmd"])); + assertThrown(parseArgs(["ldcup", "install", "gdc"])); +} + +unittest // parseArgs: uninstall without compiler leaves compilerSet false +{ + + auto a = parseArgs(["ldcup", "uninstall"]); + assert(!a.compilerSet, "compilerSet must be false when no compiler given"); + assert(a.compiler == "ldc2-latest"); +} + +unittest // parseArgs: explicit compiler for uninstall sets compilerSet +{ + + auto a = parseArgs(["ldcup", "uninstall", "ldc2-1.42.0"]); + assert(a.compilerSet); + assert(a.compiler == "ldc2-1.42.0"); +} + +unittest // detectPlatform: windows-x86_64 normalizes arch to x64 +{ + auto cm = new CompilerManager(tempDir(), "windows-x86_64"); + assert(cm.currentOS == OS.windows); + assert(cm.currentArch == Arch.x64, "x86_64 must normalize to x64 for Windows targets"); +} From 1f62f837d9b7236d3b23b9ee4ed249edafde4c3d Mon Sep 17 00:00:00 2001 From: Matheus Catarino Date: Tue, 12 May 2026 17:42:57 -0300 Subject: [PATCH 3/3] Revert "add unittests" This reverts commit a0358b8c1427eb42e011934f50612f75df24617a. --- .github/workflows/ci.yml | 64 ++++++++-------- source/app.d | 113 +++++++++++++++++++++++++++- source/impl.d | 156 --------------------------------------- 3 files changed, 141 insertions(+), 192 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c87fe77..5595782 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,14 +30,14 @@ jobs: fail-fast: false runs-on: ${{matrix.os}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: dlang-community/setup-dlang@v2 with: compiler: ldc-latest - uses: ilammy/msvc-dev-cmd@v1 if: runner.os == 'Windows' with: - arch: ${{ matrix.arch }} + arch: ${{ matrix.arch }} - name: Set CURL_CA_BUNDLE for Windows if: runner.os == 'Windows' shell: pwsh @@ -50,20 +50,17 @@ jobs: run: | echo "DUB_ARCH=--arch=arm64-windows-msvc" >> $GITHUB_ENV echo "CC=cl.exe" >> $GITHUB_ENV - echo "CXX=cl.exe" >> $GITHUB_ENV + echo "CXX=cl.exe" >> $GITHUB_ENV - name: Build - run: dub -b release ${{ env.DUB_ARCH }} - - name: Unit tests - run: dub test ${{ env.DUB_ARCH }} - - name: Integration tests - shell: bash + run: | + dub -b release ${{ env.DUB_ARCH }} + - name: Tests run: | dub -b release ${{ env.DUB_ARCH }} -- install -v - LATEST=$(dub -b release ${{ env.DUB_ARCH }} -- list | head -1) dub -b release ${{ env.DUB_ARCH }} -- install ldc2-master -v - dub -b release ${{ env.DUB_ARCH }} -- list - dub -b release ${{ env.DUB_ARCH }} -- uninstall "$LATEST" -v - dub -b release ${{ env.DUB_ARCH }} -- list + dub -b release ${{ env.DUB_ARCH }} -- list -v + dub -b release ${{ env.DUB_ARCH }} -- uninstall ldc2-1.42.0 -v + dub -b release ${{ env.DUB_ARCH }} -- list -v dub -b release ${{ env.DUB_ARCH }} -- run -v -- --version - name: Compress artifacts (Windows) if: runner.os == 'Windows' @@ -73,7 +70,7 @@ jobs: if: runner.os != 'Windows' run: | cd bin && tar -cJf ldcup-${{ matrix.os }}-${{ matrix.arch }}.tar.xz * - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') with: name: ldcup-${{ matrix.os }}-${{ matrix.arch }} @@ -92,7 +89,7 @@ jobs: fail-fast: false runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Run FreeBSD VM uses: vmactions/freebsd-vm@v1 with: @@ -101,16 +98,14 @@ jobs: pkg install -y curl dub ldc run: | dub -b release - dub test dub -b release -- install -v - LATEST=$(dub -b release -- list | head -1) dub -b release -- install ldc2-master -v - dub -b release -- list - dub -b release -- uninstall "$LATEST" -v - dub -b release -- list + dub -b release -- list -v + dub -b release -- uninstall ldc2-1.42.0 -v + dub -b release -- list -v dub -b release -- run -v -- --version tar -cJf bin/ldcup-freebsd14.3-${{ matrix.arch }}.tar.xz -C bin . - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') with: name: ldcup-freebsd-14.3-amd64 @@ -134,28 +129,27 @@ jobs: run: shell: sh steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + - name: Prepare run: | - apk update - apk add --no-cache xz tar ldc dub clang + apk update + apk add --no-cache xz tar ldc dub clang - name: Build run: | - dub -b release - touch $HOME/.profile - - name: Unit tests - run: dub test - - name: Integration tests + dub -b release + touch $HOME/.profile + - name: Tests run: | - dub -b release -- install ldc2-master -v - dub -b release -- list - source $HOME/.profile - dub -b release -- install redub -v - dub -b release -- run -v -- --version + dub -b release -- install ldc2-master -v + dub -b release -- list -v + source $HOME/.profile + dub -b release -- install redub -v + dub -b release -- run -v -- --version - name: Compress artifacts run: | - cd bin && tar -cJf ldcup-alpine-${{ matrix.arch }}.tar.xz * - - uses: actions/upload-artifact@v4 + cd bin && tar -cJf ldcup-alpine-${{ matrix.arch }}.tar.xz * + - uses: actions/upload-artifact@v7 if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') with: name: ldcup-alpine-${{ matrix.arch }} diff --git a/source/app.d b/source/app.d index 7763712..06d92bd 100644 --- a/source/app.d +++ b/source/app.d @@ -1,8 +1,119 @@ module app; import impl; +import std.stdio : writeln, writefln; +import std.string : format, startsWith, endsWith, toLower, split; +import std.algorithm : canFind, countUntil; +import std.exception : enforce; +import std.meta : AliasSeq; + +// std.algorithm.comparison.among requires an import; use a helper instead. +private bool among(T, Args...)(T val, Args choices) +{ + foreach (c; choices) + if (val == c) + return true; + return false; +} + +struct Args +{ + bool help; + bool verbose; + bool remote; + bool compilerSet; + string installDir; + string platform; + string command; + string compiler = "ldc2-latest"; + string[] compilerArgs; +} + +void printHelp(string programName) @safe +{ + writefln("Usage: %s [compiler] [options]", programName); + writeln(); + writeln("Commands:"); + writeln(" install [compiler] Install a compiler (default: ldc2-latest)"); + writeln(" uninstall Uninstall an installed compiler"); + writeln(" list List installed compilers"); + writeln(" run [compiler] -- Run ldc2 with the given flags"); + writeln(); + writeln("Compiler specifiers:"); + writeln(" ldc2-latest Latest stable LDC2 release (default)"); + writeln(" ldc2-beta Latest beta LDC2 release"); + writeln(" ldc2-nightly Latest nightly/CI build"); + writeln(" ldc2- Specific version, e.g. ldc2-1.39.0"); + writeln(" opend-latest Latest OpenD release"); + writeln(" redub Install the redub build tool"); + writeln(); + writeln("Options:"); + writeln(" --install-dir=DIR Override installation directory"); + writeln(" --platform=OS-ARCH Override platform (e.g. linux-x86_64)"); + writeln(" --remote (list) Show all available remote releases"); + writeln(" --verbose, -v Enable verbose output"); + writeln(" --help, -h Show this help message"); +} + +/// Strip a leading "v" from a version tag so "v1.39.0" → "ldc2-1.39.0". +private string normaliseCompilerSpec(string spec) @safe pure +{ + // e.g. user typed "v1.39.0" — treat as "ldc2-1.39.0" + if (spec.startsWith("v") && !spec.startsWith("verbose")) + return "ldc2-" ~ spec[1 .. $]; + return spec; +} + +Args parseArgs(string[] argv) @safe +{ + Args parsed; + + for (size_t i = 1; i < argv.length; ++i) + { + string arg = argv[i]; + + if (arg == "--help" || arg == "-h") + parsed.help = true; + else if (arg == "--verbose" || arg == "-v") + parsed.verbose = true; + else if (arg == "--remote") + parsed.remote = true; + else if (arg.startsWith("--platform=")) + parsed.platform = arg["--platform=".length .. $]; + else if (arg.startsWith("--install-dir=")) + parsed.installDir = arg["--install-dir=".length .. $]; + else if (arg == "--") + { + // Everything after "--" is passed verbatim to the compiler. + parsed.compilerArgs = argv[i + 1 .. $]; + break; + } + else if (arg.startsWith("ldc2-") || arg.startsWith("opend-")) + { + parsed.compiler = normaliseCompilerSpec(arg); + parsed.compilerSet = true; + } + else if (arg == "redub") + { + parsed.compiler = arg; + parsed.compilerSet = true; + } + else if (arg.among("dmd", "gdc")) + throw new Exception("Only ldc2 and opend compilers are supported."); + else if (arg.startsWith("v") && arg.length > 1 && arg[1].isDigit) + { + parsed.compiler = normaliseCompilerSpec(arg); + parsed.compilerSet = true; + } + else if (parsed.command.empty) + parsed.command = arg; + else + throw new Exception("Unknown argument: " ~ arg); + } + + return parsed; +} -version(unittest) {} else int main(string[] argv) @safe { Args parsed; diff --git a/source/impl.d b/source/impl.d index 9ed3b29..321b9b7 100644 --- a/source/impl.d +++ b/source/impl.d @@ -52,109 +52,6 @@ private string stripLeadingV(string s) @safe pure return (s.length > 0 && s[0] == 'v') ? s[1 .. $] : s; } -private bool among(T, Choices...)(T val, Choices choices) -{ - foreach (c; choices) - if (val == c) - return true; - return false; -} - -struct Args -{ - bool help; - bool verbose; - bool remote; - bool compilerSet; - string installDir; - string platform; - string command; - string compiler = "ldc2-latest"; - string[] compilerArgs; -} - -void printHelp(string programName) @safe -{ - writefln("Usage: %s [compiler] [options]", programName); - writeln(); - writeln("Commands:"); - writeln(" install [compiler] Install a compiler (default: ldc2-latest)"); - writeln(" uninstall Uninstall an installed compiler"); - writeln(" list List installed compilers"); - writeln(" run [compiler] -- Run ldc2 with the given flags"); - writeln(); - writeln("Compiler specifiers:"); - writeln(" ldc2-latest Latest stable LDC2 release (default)"); - writeln(" ldc2-beta Latest beta LDC2 release"); - writeln(" ldc2-nightly Latest nightly/CI build"); - writeln(" ldc2- Specific version, e.g. ldc2-1.39.0"); - writeln(" opend-latest Latest OpenD release"); - writeln(" redub Install the redub build tool"); - writeln(); - writeln("Options:"); - writeln(" --install-dir=DIR Override installation directory"); - writeln(" --platform=OS-ARCH Override platform (e.g. linux-x86_64)"); - writeln(" --remote (list) Show all available remote releases"); - writeln(" --verbose, -v Enable verbose output"); - writeln(" --help, -h Show this help message"); -} - -private string normaliseCompilerSpec(string spec) @safe pure -{ - if (spec.startsWith("v") && !spec.startsWith("verbose")) - return "ldc2-" ~ spec[1 .. $]; - return spec; -} - -Args parseArgs(string[] argv) @safe -{ - Args parsed; - - for (size_t i = 1; i < argv.length; ++i) - { - string arg = argv[i]; - - if (arg == "--help" || arg == "-h") - parsed.help = true; - else if (arg == "--verbose" || arg == "-v") - parsed.verbose = true; - else if (arg == "--remote") - parsed.remote = true; - else if (arg.startsWith("--platform=")) - parsed.platform = arg["--platform=".length .. $]; - else if (arg.startsWith("--install-dir=")) - parsed.installDir = arg["--install-dir=".length .. $]; - else if (arg == "--") - { - parsed.compilerArgs = argv[i + 1 .. $]; - break; - } - else if (arg.startsWith("ldc2-") || arg.startsWith("opend-")) - { - parsed.compiler = normaliseCompilerSpec(arg); - parsed.compilerSet = true; - } - else if (arg == "redub") - { - parsed.compiler = arg; - parsed.compilerSet = true; - } - else if (arg.among("dmd", "gdc")) - throw new Exception("Only ldc2 and opend compilers are supported."); - else if (arg.startsWith("v") && arg.length > 1 && arg[1].isDigit) - { - parsed.compiler = normaliseCompilerSpec(arg); - parsed.compilerSet = true; - } - else if (parsed.command.empty) - parsed.command = arg; - else - throw new Exception("Unknown argument: " ~ arg); - } - - return parsed; -} - class CompilerManager { private: @@ -827,56 +724,3 @@ string findProgram(string programName) @safe } throw new Exception("Program not found in PATH: " ~ programName); } - -// ─── Unit tests ──────────────────────────────────────────────────────────── - -unittest // ext property: runtime value based on currentOS, not compile-time host -{ - auto win = new CompilerManager(tempDir(), "windows-x64"); - assert(win.ext == ".7z", "Windows target must use .7z"); - - auto lin = new CompilerManager(tempDir(), "linux-x86_64"); - assert(lin.ext == ".tar.xz", "Linux target must use .tar.xz"); - - auto osx = new CompilerManager(tempDir(), "osx-universal"); - assert(osx.ext == ".tar.xz", "macOS target must use .tar.xz"); -} - -unittest // parseArgs: v-prefix version normalized to ldc2- -{ - - auto a = parseArgs(["ldcup", "install", "v1.42.0"]); - assert(a.compiler == "ldc2-1.42.0", "v1.42.0 must become ldc2-1.42.0"); - assert(a.compilerSet, "compilerSet must be true for explicit version"); -} - -unittest // parseArgs: dmd and gdc rejected with exception -{ - - import std.exception : assertThrown; - assertThrown(parseArgs(["ldcup", "install", "dmd"])); - assertThrown(parseArgs(["ldcup", "install", "gdc"])); -} - -unittest // parseArgs: uninstall without compiler leaves compilerSet false -{ - - auto a = parseArgs(["ldcup", "uninstall"]); - assert(!a.compilerSet, "compilerSet must be false when no compiler given"); - assert(a.compiler == "ldc2-latest"); -} - -unittest // parseArgs: explicit compiler for uninstall sets compilerSet -{ - - auto a = parseArgs(["ldcup", "uninstall", "ldc2-1.42.0"]); - assert(a.compilerSet); - assert(a.compiler == "ldc2-1.42.0"); -} - -unittest // detectPlatform: windows-x86_64 normalizes arch to x64 -{ - auto cm = new CompilerManager(tempDir(), "windows-x86_64"); - assert(cm.currentOS == OS.windows); - assert(cm.currentArch == Arch.x64, "x86_64 must normalize to x64 for Windows targets"); -}