From 6e09e24a5f6d19ac1d14905201b18d6f8020b554 Mon Sep 17 00:00:00 2001 From: Daylin Morgan Date: Wed, 25 Mar 2026 13:25:06 -0400 Subject: [PATCH 1/8] feat(#1450): add --skipBin to limit compilation of hybrid deps --- src/nimblepkg/options.nim | 4 ++ src/nimblepkg/vnext.nim | 15 ++++---- .../pkgWithHybridDep/pkgWithHybridDep.nimble | 14 +++++++ .../pkgWithHybridDep/src/pkgWithHybridDep.nim | 5 +++ tests/tester.nim | 1 + tests/tskipbin.nim | 37 +++++++++++++++++++ 6 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 tests/pkgWithHybridDep/pkgWithHybridDep.nimble create mode 100644 tests/pkgWithHybridDep/src/pkgWithHybridDep.nim create mode 100644 tests/tskipbin.nim diff --git a/src/nimblepkg/options.nim b/src/nimblepkg/options.nim index 06b2f6653..4547b212a 100644 --- a/src/nimblepkg/options.nim +++ b/src/nimblepkg/options.nim @@ -47,6 +47,7 @@ type noColor*: bool disableValidation*: bool continueTestsOnFailure*: bool + skipBin*: bool # Whether to skip compilation of binaries for hybrid dependencies. ## Whether packages' repos should always be downloaded with their history. forceFullClone*: bool # Temporary storage of flags that have not been captured by any specific Action. @@ -295,6 +296,7 @@ Nimble Options: --features Activate features. Only used when using the declarative parser. --ignoreSubmodules Ignore submodules when cloning a repository. --asyncdownloads Use async for package downloads. (temporary flag) + --skipBin Skip compilation of binaries of hybrid dependencies. For more information read the GitHub readme: https://github.com/nim-lang/nimble#readme """ @@ -787,6 +789,8 @@ proc parseFlag*(flag, val: string, result: var Options, kind = cmdLongOption) = result.useAsyncDownloads = true of "lenient": result.lenient = true + of "skipbin": + result.skipBin = true else: isGlobalFlag = false var wasFlagHandled = true diff --git a/src/nimblepkg/vnext.nim b/src/nimblepkg/vnext.nim index 5c1779b7d..bf89323e0 100644 --- a/src/nimblepkg/vnext.nim +++ b/src/nimblepkg/vnext.nim @@ -808,13 +808,14 @@ proc installFromDirDownloadInfo(nimBin: string, downloadDir: string, url: string # Don't copy artifacts if project local deps mode and "installing" the top level package. if not (options.localdeps and options.isInstallingTopLevel(dir)): var filesInstalled: HashSet[string] - let hasBinaries = pkgInfo.bin.len > 0 and not pkgInfo.basicInfo.name.isNim + let shouldBuildBinaries = pkgInfo.bin.len > 0 and not pkgInfo.basicInfo.name.isNim and + (not options.skipBin or options.isInstallingTopLevel(dir)) let hasPreInstallHook = pkgInfo.hasBeforeInstallHook and not pkgInfo.basicInfo.name.isNim # Install pipeline: workDir → before-install hook → build → copy to pkgDestDir → after-install hook # Optimization: skip buildtemp when we know it's safe (no binaries, no before-install hook, no submodules) let hasSubmodules = not options.ignoreSubmodules and fileExists(downloadDir / ".gitmodules") - let canSkipBuildTemp = not hasBinaries and not hasPreInstallHook and not hasSubmodules + let canSkipBuildTemp = not shouldBuildBinaries and not hasPreInstallHook and not hasSubmodules var workDir, buildTempDir: string var workPkgInfo: PackageInfo @@ -825,7 +826,7 @@ proc installFromDirDownloadInfo(nimBin: string, downloadDir: string, url: string workPkgInfo = pkgInfo else: display("Info:", "Using buildtemp for " & pkgInfo.basicInfo.name & - " (binaries: " & $hasBinaries & ", before-install hook: " & $hasPreInstallHook & + " (binaries: " & $shouldBuildBinaries & ", before-install hook: " & $hasPreInstallHook & ", submodules: " & $hasSubmodules & ")", priority = LowPriority) buildTempDir = options.getPkgBuildTempDir( @@ -882,7 +883,7 @@ proc installFromDirDownloadInfo(nimBin: string, downloadDir: string, url: string executeHook(nimBin, workDir, options, actionInstall, before = true) # Build binaries (only if there are any) - if hasBinaries: + if shouldBuildBinaries: let paths = getPathsAllPkgs(options, nimBin) let flags = if options.action.typ in {actionInstall, actionPath, actionUninstall, actionDevelop}: options.action.passNimFlags @@ -921,7 +922,7 @@ proc installFromDirDownloadInfo(nimBin: string, downloadDir: string, url: string filesInstalled.incl copyFileD(workPkgInfo.myPath, nimbleFileDest) # Copy built binaries (only if there are any) - if hasBinaries: + if shouldBuildBinaries: for bin, src in workPkgInfo.bin: let binDest = if dirExists(pkgDestDir / bin): bin & ".out" else: bin let srcBin = workPkgInfo.getOutputDir(bin) @@ -940,7 +941,7 @@ proc installFromDirDownloadInfo(nimBin: string, downloadDir: string, url: string executeHook(nimBin, pkgDestDir, options, actionInstall, before = false) # Create bin symlinks (only if there are binaries) - if hasBinaries: + if shouldBuildBinaries: createBinSymlink(pkgInfo, options) finally: @@ -1432,7 +1433,7 @@ proc installPkgs*(satResult: var SATResult, options: var Options, nimBin: string if isRoot and options.action.typ in rootBuildActions: buildPkg(nimBin, pkgToBuild, isRoot, options) satResult.buildPkgs.add(pkgToBuild) - elif pkgToBuild.isLink: + elif pkgToBuild.isLink and not options.skipBin: # Build develop mode packages buildPkg(nimBin, pkgToBuild, false, options) satResult.buildPkgs.add(pkgToBuild) diff --git a/tests/pkgWithHybridDep/pkgWithHybridDep.nimble b/tests/pkgWithHybridDep/pkgWithHybridDep.nimble new file mode 100644 index 000000000..a03595c72 --- /dev/null +++ b/tests/pkgWithHybridDep/pkgWithHybridDep.nimble @@ -0,0 +1,14 @@ +# Package + +version = "0.1.0" +author = "test" +description = "A new awesome nimble package" +license = "MIT" +srcDir = "src" +bin = @["pkgWithHybridDep"] + + +# Dependencies + +requires "nim >= 2.2.4" +requires "https://github.com/nim-lang/nimble?subdir=tests/develop/hybrid" diff --git a/tests/pkgWithHybridDep/src/pkgWithHybridDep.nim b/tests/pkgWithHybridDep/src/pkgWithHybridDep.nim new file mode 100644 index 000000000..862d40c24 --- /dev/null +++ b/tests/pkgWithHybridDep/src/pkgWithHybridDep.nim @@ -0,0 +1,5 @@ +# This is just an example to get you started. A typical binary package +# uses this file as the main entry point of the application. + +when isMainModule: + echo("Hello, World!") diff --git a/tests/tester.nim b/tests/tester.nim index 58e44e97c..068a5c5b4 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -39,6 +39,7 @@ import tfilepathrequires import tglobalinstall import tasynctools import tbuildinstall +import tskipbin # # nonim tests are very slow and (often) break the CI. # # import tnonim diff --git a/tests/tskipbin.nim b/tests/tskipbin.nim new file mode 100644 index 000000000..f23cf4636 --- /dev/null +++ b/tests/tskipbin.nim @@ -0,0 +1,37 @@ +# Tests that with --skipBin hybrid packages ares not compiled +# packages are built in a temp directory and only necessary files are installed + +{.used.} + +import unittest, os, strutils +import testscommon +from nimble import nimblePathsFileName, nimbleConfigFileName +from nimblepkg/common import cd + +func exe(name: string): string = + when defined(windows): name & ".exe" else: name + +template checkSkip(cmd: string, toSkip = false) = + cd "pkgWithHybridDep": + cleanDir installDir + cleanFiles nimblePathsFileName, nimbleConfigFileName + var args = @[cmd] + if toSkip: args.add "--skipBin" + let res = execNimbleYes(args) + verify res + let hybridPkgDir = getPackageDir(pkgsDir, "hybrid") + check hybridPkgDir.len > 0 + check "Building hybrid/hybrid" in res.output != toSkip + check fileExists(hybridPkgDir / "hybrid".exe) != toSkip + check fileExists(installDir / "bin" / "hybrid".exe ) != toSkip + # --skipBin should not effect the root package + if cmd == "install": + check fileExists(installDir / "bin" / "pkgWithHybridDep".exe) + +suite "--skipBin": + for command in ["setup", "install"]: + test command & " without --skipBin": + checkSkip(command, false) + test command & " with --skipBin": + checkSkip(command, true) + From ad98db5a9234bbc87b48424398f21d895dc9a712 Mon Sep 17 00:00:00 2001 From: Daylin Morgan Date: Sat, 28 Mar 2026 09:31:39 -0400 Subject: [PATCH 2/8] windows stub doesn't use .exe --- tests/tskipbin.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tskipbin.nim b/tests/tskipbin.nim index f23cf4636..838a46cfc 100644 --- a/tests/tskipbin.nim +++ b/tests/tskipbin.nim @@ -23,10 +23,10 @@ template checkSkip(cmd: string, toSkip = false) = check hybridPkgDir.len > 0 check "Building hybrid/hybrid" in res.output != toSkip check fileExists(hybridPkgDir / "hybrid".exe) != toSkip - check fileExists(installDir / "bin" / "hybrid".exe ) != toSkip + check fileExists(installDir / "bin" / "hybrid") != toSkip # --skipBin should not effect the root package if cmd == "install": - check fileExists(installDir / "bin" / "pkgWithHybridDep".exe) + check fileExists(installDir / "bin" / "pkgWithHybridDep") suite "--skipBin": for command in ["setup", "install"]: From e10b0c799a17de20caec4e514802b11b4d8893b2 Mon Sep 17 00:00:00 2001 From: Daylin Morgan Date: Sun, 29 Mar 2026 18:14:44 -0400 Subject: [PATCH 3/8] refactor tests for the serial run --- tests/tskipbin.nim | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/tests/tskipbin.nim b/tests/tskipbin.nim index 838a46cfc..fb9f14267 100644 --- a/tests/tskipbin.nim +++ b/tests/tskipbin.nim @@ -1,5 +1,4 @@ -# Tests that with --skipBin hybrid packages ares not compiled -# packages are built in a temp directory and only necessary files are installed +# Tests that with --skipBin hybrid packages are not compiled {.used.} @@ -12,26 +11,33 @@ func exe(name: string): string = when defined(windows): name & ".exe" else: name template checkSkip(cmd: string, toSkip = false) = - cd "pkgWithHybridDep": - cleanDir installDir - cleanFiles nimblePathsFileName, nimbleConfigFileName - var args = @[cmd] - if toSkip: args.add "--skipBin" - let res = execNimbleYes(args) - verify res - let hybridPkgDir = getPackageDir(pkgsDir, "hybrid") - check hybridPkgDir.len > 0 - check "Building hybrid/hybrid" in res.output != toSkip - check fileExists(hybridPkgDir / "hybrid".exe) != toSkip - check fileExists(installDir / "bin" / "hybrid") != toSkip - # --skipBin should not effect the root package - if cmd == "install": - check fileExists(installDir / "bin" / "pkgWithHybridDep") + var args = @[cmd] + if toSkip: args.add "--skipBin" + let res = execNimbleYes(args) + verify res + let hybridPkgDir = getPackageDir(pkgsDir, "hybrid") + check hybridPkgDir.len > 0 + check "Building hybrid/hybrid" in res.output != toSkip + check fileExists(hybridPkgDir / "hybrid".exe) != toSkip + check fileExists(installDir / "bin" / "hybrid") != toSkip + # --skipBin should not effect the root package + if cmd == "install": + check fileExists(installDir / "bin" / "pkgWithHybridDep") + +template runTest(name: string, body: untyped) = + test name: + cd "pkgWithHybridDep": + cleanDir installDir + cleanFiles nimblePathsFileName, nimbleConfigFileName + body suite "--skipBin": for command in ["setup", "install"]: - test command & " without --skipBin": + runTest command & " without --skipBin": checkSkip(command, false) - test command & " with --skipBin": + runTest command & " with --skipBin": + checkSkip(command, true) + runTest command & " with --skipBin then without --skipBin": checkSkip(command, true) + checkSkip(command, false) From 407808c1824f004b7535499974aaf22d520893d4 Mon Sep 17 00:00:00 2001 From: Daylin Morgan Date: Sun, 29 Mar 2026 18:42:49 -0400 Subject: [PATCH 4/8] ensure that bins always exist without --skipBin --- src/nimblepkg/vnext.nim | 22 ++++++++++++++++++++-- tests/tskipbin.nim | 6 +----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/nimblepkg/vnext.nim b/src/nimblepkg/vnext.nim index bf89323e0..052587bcc 100644 --- a/src/nimblepkg/vnext.nim +++ b/src/nimblepkg/vnext.nim @@ -702,16 +702,26 @@ proc executeHook(nimBin: string, dir: string, options: var Options, action: Acti else: raise nimbleError("Post-hook prevented further execution.") +proc binsExist(pkgInfo: PackageInfo, options: Options): bool = + let needsBinaries = pkgInfo.bin.len > 0 and not pkgInfo.basicInfo.name.isNim and not options.skipBin + if needsBinaries: + for bin in pkgInfo.bin.keys: + let binPath = pkgInfo.getOutputDir(bin) + if not fileExists(binPath): + return false + result = true + proc packageExists(nimBin: string, pkgInfo: PackageInfo, options: Options): Option[PackageInfo] = ## Checks whether a package `pkgInfo` already exists in the Nimble cache. If a ## package already exists returns the `PackageInfo` of the package in the - ## cache otherwise returns `none`. Raises a `NimbleError` in the case the - ## package exists in the cache but it is not valid. + ## cache otherwise returns `none`. If a package exits but is missing expected binaries also returns `none`. + ## Raises a `NimbleError` in the case the package exists in the cache but it is not valid. ## ## Also checks for packages with the same name and checksum but different version ## to avoid storing the same content multiple times with different version labels. let pkgDestDir = pkgInfo.getPkgDest(options) + if fileExists(pkgDestDir / packageMetaDataFileName): var oldPkgInfo = initPackageInfo() try: @@ -720,6 +730,10 @@ proc packageExists(nimBin: string, pkgInfo: PackageInfo, options: Options): raise nimbleError(&"The package inside \"{pkgDestDir}\" is invalid.", details = error) fillMetaData(oldPkgInfo, pkgDestDir, true, options) + + if not binsExist(pkgInfo, options): + return none(PackageInfo) + return some(oldPkgInfo) # Check if a package with the same name and checksum exists with a different version. @@ -740,6 +754,10 @@ proc packageExists(nimBin: string, pkgInfo: PackageInfo, options: Options): except CatchableError: continue # Skip invalid packages fillMetaData(oldPkgInfo, path, true, options) + + if not binsExist(pkgInfo, options): + return none(PackageInfo) + return some(oldPkgInfo) return none[PackageInfo]() diff --git a/tests/tskipbin.nim b/tests/tskipbin.nim index fb9f14267..654fbf5da 100644 --- a/tests/tskipbin.nim +++ b/tests/tskipbin.nim @@ -33,11 +33,7 @@ template runTest(name: string, body: untyped) = suite "--skipBin": for command in ["setup", "install"]: - runTest command & " without --skipBin": - checkSkip(command, false) - runTest command & " with --skipBin": - checkSkip(command, true) - runTest command & " with --skipBin then without --skipBin": + runTest command & " with --skipBin & without --skipBin": checkSkip(command, true) checkSkip(command, false) From b32b4c3fc060a3cb540ddb10f7a71f294ddb53df Mon Sep 17 00:00:00 2001 From: Daylin Morgan Date: Mon, 30 Mar 2026 05:16:12 +0000 Subject: [PATCH 5/8] `nimble uninstall` reads `.nimble` files using nimscript unrelated test failures were triggered by a missing nim to execute nimscript to downgrade a removed package --- src/nimble.nim | 2 +- src/nimblepkg/nimscriptwrapper.nim | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nimble.nim b/src/nimble.nim index 9387fd29a..49e8d8cab 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -2270,7 +2270,7 @@ when isMainModule: # Actions that don't need a Nim binary should not trigger downloading Nim. # This avoids e.g. `nimble --version` or `nimble list -i` fetching Nim binaries. const actionsNotNeedingNim = {actionRefresh, actionSearch, actionList, - actionPath, actionUninstall, actionClean, actionManual, + actionPath, actionClean, actionManual, actionNil} let needsNim = not opt.showVersion and not opt.showHelp and opt.action.typ notin actionsNotNeedingNim diff --git a/src/nimblepkg/nimscriptwrapper.nim b/src/nimblepkg/nimscriptwrapper.nim index e457ad896..c985b0c6e 100644 --- a/src/nimblepkg/nimscriptwrapper.nim +++ b/src/nimblepkg/nimscriptwrapper.nim @@ -43,6 +43,7 @@ proc getNimblecache(): string = proc execNimscript(nimBin: string, nimbleFile, nimsFile, actionName: string, options: Options, isHook: bool ): tuple[output: string, exitCode: int, stdout: string] = + assert nimBin != "" let outFile = getNimbleTempDir() & ".out" isCustomTask = isCustomTask(actionName, options) From f9c562244e734b2be6bae581dea06395416d907a Mon Sep 17 00:00:00 2001 From: Daylin Morgan Date: Tue, 31 Mar 2026 16:03:18 -0400 Subject: [PATCH 6/8] just use return instead --- src/nimblepkg/vnext.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nimblepkg/vnext.nim b/src/nimblepkg/vnext.nim index 052587bcc..53257b928 100644 --- a/src/nimblepkg/vnext.nim +++ b/src/nimblepkg/vnext.nim @@ -709,7 +709,7 @@ proc binsExist(pkgInfo: PackageInfo, options: Options): bool = let binPath = pkgInfo.getOutputDir(bin) if not fileExists(binPath): return false - result = true + return true proc packageExists(nimBin: string, pkgInfo: PackageInfo, options: Options): Option[PackageInfo] = From 5667f377769eea27d5e9965e37644f9c7881ecca Mon Sep 17 00:00:00 2001 From: Daylin Morgan Date: Tue, 31 Mar 2026 16:09:13 -0400 Subject: [PATCH 7/8] typo in doc comment --- src/nimblepkg/vnext.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nimblepkg/vnext.nim b/src/nimblepkg/vnext.nim index 53257b928..1966d2727 100644 --- a/src/nimblepkg/vnext.nim +++ b/src/nimblepkg/vnext.nim @@ -715,7 +715,7 @@ proc packageExists(nimBin: string, pkgInfo: PackageInfo, options: Options): Option[PackageInfo] = ## Checks whether a package `pkgInfo` already exists in the Nimble cache. If a ## package already exists returns the `PackageInfo` of the package in the - ## cache otherwise returns `none`. If a package exits but is missing expected binaries also returns `none`. + ## cache otherwise returns `none`. If a package exists but is missing expected binaries also returns `none`. ## Raises a `NimbleError` in the case the package exists in the cache but it is not valid. ## ## Also checks for packages with the same name and checksum but different version From 7ae5a3aa8c055f1af4f38513c46a1ccc9d8a91f1 Mon Sep 17 00:00:00 2001 From: Daylin Morgan Date: Tue, 31 Mar 2026 16:09:13 -0400 Subject: [PATCH 8/8] better suite name; drop templates from tskipbin --- tests/tskipbin.nim | 51 +++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/tests/tskipbin.nim b/tests/tskipbin.nim index 654fbf5da..f48c2ba42 100644 --- a/tests/tskipbin.nim +++ b/tests/tskipbin.nim @@ -7,33 +7,24 @@ import testscommon from nimble import nimblePathsFileName, nimbleConfigFileName from nimblepkg/common import cd -func exe(name: string): string = - when defined(windows): name & ".exe" else: name - -template checkSkip(cmd: string, toSkip = false) = - var args = @[cmd] - if toSkip: args.add "--skipBin" - let res = execNimbleYes(args) - verify res - let hybridPkgDir = getPackageDir(pkgsDir, "hybrid") - check hybridPkgDir.len > 0 - check "Building hybrid/hybrid" in res.output != toSkip - check fileExists(hybridPkgDir / "hybrid".exe) != toSkip - check fileExists(installDir / "bin" / "hybrid") != toSkip - # --skipBin should not effect the root package - if cmd == "install": - check fileExists(installDir / "bin" / "pkgWithHybridDep") - -template runTest(name: string, body: untyped) = - test name: - cd "pkgWithHybridDep": - cleanDir installDir - cleanFiles nimblePathsFileName, nimbleConfigFileName - body - -suite "--skipBin": - for command in ["setup", "install"]: - runTest command & " with --skipBin & without --skipBin": - checkSkip(command, true) - checkSkip(command, false) - +suite "skip compilation of hybrid packages with --skipBin": + for cmd in ["setup", "install"]: + test cmd & " with --skipBin then without --skipBin": + cd "pkgWithHybridDep": + cleanDir installDir + cleanFiles nimblePathsFileName, nimbleConfigFileName + for toSkip in [true, false]: + var args = @[cmd] + if toSkip: args.add "--skipBin" + let res = execNimbleYes(args) + verify res + let hybridPkgDir = getPackageDir(pkgsDir, "hybrid") + check hybridPkgDir.len > 0 + check "Building hybrid/hybrid" in res.output != toSkip + check fileExists( + hybridPkgDir / "hybrid" & (when defined(windows): ".exe" else: "") + ) != toSkip + check fileExists(installDir / "bin" / "hybrid") != toSkip + # --skipBin should not effect the root package + if cmd == "install": + check fileExists(installDir / "bin" / "pkgWithHybridDep")