diff --git a/.forge.cfg b/.forge.cfg new file mode 100644 index 0000000..83f7b68 --- /dev/null +++ b/.forge.cfg @@ -0,0 +1,13 @@ +# example ccnz config file +nimble + +[target] +x86_64-linux-gnu +x86_64-linux-musl +x86_64-macos-none +x86_64-windows-gnu + +[bin] +src/forge +src/forgecc + diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 04b004d..c505de8 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -24,8 +24,9 @@ jobs: name: check latest commit is less than a day if: ${{ github.event_name == 'schedule' }} run: | - test -n "$(git rev-list --since="24 hours" HEAD)" \ - && echo "quit=true" >> "$GITHUB_OUTPUT" + if [[ -n "$(git rev-list --since='24 hours' HEAD)" ]]; then + echo "quit=true" >> "$GITHUB_OUTPUT" + fi build-artifacts: needs: check-commits diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8c05f2e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Daylin Morgan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 3f89bfa..d661fc9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ -# ccnz compiles nim w/zig +# forge +A toolchain to forge your multi-platform `nim` binaries. + +## install + +```sh +nimble install https://github.com/daylinmorgan/forge +``` diff --git a/ccnz.nimble b/ccnz.nimble deleted file mode 100644 index 92c43df..0000000 --- a/ccnz.nimble +++ /dev/null @@ -1,46 +0,0 @@ -version = "2023.1001" -author = "Daylin Morgan" -description = "ccnz compiles nim w/zig" -license = "MIT" -srcDir = "src" -bin = @["ccnz", "ccnzcc"] -binDir = "bin" - - -requires "nim >= 2.0.0", - "cligen" - - - -import strformat -const targets = [ - "x86_64-linux-gnu", - "x86_64-linux-musl", - "x86_64-macos-none", - "x86_64-windows-gnu" - ] - -task release, "build release assets": - mkdir "dist" - for target in targets: - let ext = if target == "x86_64-windows-gnu": ".cmd" else: "" - for app in @["ccnz", "ccnzcc"]: - let outdir = &"dist/{target}/" - exec &"ccnz cc --target {target} --nimble -- --out:{outdir}{app}{ext} -d:release src/{app}" - -task bundle, "package build assets": - cd "dist" - for target in targets: - let - app = projectName() - cmd = - if target == "x86_64-windows-gnu": - &"7z a {app}_{target}.zip {target}" - else: - &"tar czf {app}_{target}.tar.gz {target}" - - cpFile("../README.md", &"{target}/README.md") - exec cmd - - - diff --git a/forge.nimble b/forge.nimble new file mode 100644 index 0000000..778a85c --- /dev/null +++ b/forge.nimble @@ -0,0 +1,30 @@ +version = "2023.1001" +author = "Daylin Morgan" +description = "build nim binaries for all the platforms" +license = "MIT" +srcDir = "src" +bin = @["forge", "forgecc"] +binDir = "bin" + + +requires "nim >= 2.0.0", + "cligen" + +import strformat + +task release, "build release assets": + version = (gorgeEx "git describe --tags --always").output + exec &"forge release -v {version} -V" + +task bundle, "package build assets": + withDir "dist": + for dir in listDirs("."): + let cmd = if "windows" in dir: + &"7z a {dir}.zip {dir}" + else: + &"tar czf {dir}.tar.gz {dir}" + cpFile("../README.md", &"{dir}/README.md") + exec cmd + + + diff --git a/src/ccnz.nim b/src/ccnz.nim deleted file mode 100644 index a944ff9..0000000 --- a/src/ccnz.nim +++ /dev/null @@ -1,72 +0,0 @@ -import std/[osproc, strformat, strutils, tables, terminal] -import ccnz/utils - -proc genFlags(target: string, args: seq[string]): seq[string] = - let targetList = zigTargets() - if target notin targetList: - errQuit &"unknown target: {target}", "", "must be one of:", - targetList.columns - - addFlag "cpu" - addFlag "os" - - result &= @[ - "--cc:clang", - &"--clang.exe='ccnzcc'", - &"--clang.linkerexe='ccnzcc'", - # &"--passC:\"-target {target} -fno-sanitize=undefined\"", - &"--passC:'-target {target}'", - # &"--passL:\"-target {target} -fno-sanitize=undefined\"", - &"--passL:'-target {target}'", - ] - -proc targets() = - ## show available targets - let targetList = zigTargets() - styledEcho styleBright, fgGreen, "available targets:" - echo targetList.columns - -proc cc(target: string, dryrun: bool = false, nimble: bool = false, args: seq[string]) = - ## compile with zig cc - let ccArgs = genFlags(target, args) - if args.len == 0: - errQuit "expected additional arguments i.e. -- -d:release src/main.nim\n" - - let rest = - if args[0] == "c": - args[1..^1] - else: - args - - let baseCmd = if nimble: "nimble" else: "nim" - let cmd = (@[baseCmd] & @["c"] & ccArgs & rest).join(" ") - if dryrun: - stderr.write cmd, "\n" - else: - quit(execCmd cmd) - -when isMainModule: - import cligen - zigExists() - - const - customMulti = "${doc}Usage:\n $command {SUBCMD} [sub-command options & parameters]\n\nsubcommands:\n$subcmds" - vsn = staticExec "git describe --tags --always HEAD" - - - if clCfg.useMulti == "": clCfg.useMulti = customMulti - if clCfg.helpAttr.len == 0: - clCfg.helpAttr = {"cmd": "\e[1;36m", "clDescrip": "", "clDflVal": "\e[33m", - "clOptKeys": "\e[32m", "clValType": "\e[31m", "args": "\e[3m"}.toTable - clCfg.helpAttrOff = {"cmd": "\e[m", "clDescrip": "\e[m", "clDflVal": "\e[m", - "clOptKeys": "\e[m", "clValType": "\e[m", "args": "\e[m"}.toTable - - var vsnCfg = clCfg - vsnCfg.version = vsn - - - dispatchMulti(["multi", cf = vsnCfg], [cc, help = { - "dryrun": "show command instead of executing", - "nimble": "use nimble as base command for compiling" - }, short = {"dryrun": 'n'}], - [targets]) diff --git a/src/ccnzcc.nim b/src/ccnzcc.nim deleted file mode 100644 index 6cb4bfc..0000000 --- a/src/ccnzcc.nim +++ /dev/null @@ -1,3 +0,0 @@ -import ccnz/utils - -callZig("cc") diff --git a/src/forge.nim b/src/forge.nim new file mode 100644 index 0000000..d05d4d2 --- /dev/null +++ b/src/forge.nim @@ -0,0 +1,155 @@ +import std/[os, osproc, strformat, strutils, tables, terminal] + +import forge/[config, utils, term] + +proc genFlags(triplet: Triplet, args: seq[string] = @[]): seq[string] = + addFlag "cpu" + addFlag "os" + + result &= @[ + "--cc:clang", + &"--clang.exe='forgecc'", + &"--clang.linkerexe='forgecc'", + # &"--passC:\"-target {target} -fno-sanitize=undefined\"", + &"--passC:'-target {triplet}'", + # &"--passL:\"-target {target} -fno-sanitize=undefined\"", + &"--passL:'-target {triplet}'", + ] + +proc targets() = + ## show available targets + let targetList = zigTargets() + termEcho styleBright, fgGreen, "available targets:" + stderr.writeLine targetList.columns + +proc cc(target: string, dryrun: bool = false, nimble: bool = false, args: seq[string]) = + ## compile with zig cc + if args.len == 0: + termErrQuit "expected additional arguments i.e. -- -d:release src/main.nim" + + let + targets = zigTargets() + ccArgs = genFlags(parseTriplet(target, targets), args) + rest = parseArgs(args) + baseCmd = if nimble: "nimble" else: "nim" + cmd = (@[baseCmd] & @["c"] & ccArgs & rest).join(" ") + + if dryrun: + stderr.writeLine cmd + else: + quit(execCmd cmd) + +proc release( + target: seq[string] = @[], + bin: seq[string] = @[], + args: seq[string], + outdir: string = "dist", + format: string = "", + name: string = "", + version: string = "", + dryrun: bool = false, + nimble: bool = false, + configFile: string = ".forge.cfg", + verbose: bool = false, + ) = + ## generate release assets for n>=1 targets + ## + ## format argument: + ## format is a template string used for each target directory + ## available fields are name, version, target + ## default: ${name}-v${verison}-${target} + ## + ## if name or version are not specified they will be inferred from the local .nimble file + + var cfg = newConfig( + target, + bin, + outdir, + format, + name, + version, + nimble, + configFile + ) + if cfg.targets.len == 0: + termErrQuit "expected at least 1 target" + if cfg.bins.len == 0: + termErrQuit "expected at least 1 bin" + + if verbose: + termEcho $cfg + + if dryrun: + termEcho styleBright, fgBlue, "dry run...see below for commands" + + let + targets = zigTargets() + baseCmd = if nimble or cfg.nimble: "nimble" else: "nim" + rest = parseArgs(args) + + termEcho styleBright, fgYellow, + &"compiling {cfg.bins.len} binaries for {cfg.targets.len} targets" + + for t, tArgs in cfg.targets: + for b, bArgs in cfg.bins: + var cmdParts: seq[string] = @[] + let outFlag = &"--outdir:'" & ( + outdir / formatDirName(cfg.format, cfg.name, cfg.version, t) + ) & "'" + + cmdParts &= @[baseCmd, "c"] + cmdParts.add genFlags(parseTriplet(t, targets), rest) + cmdParts.add "-d:release" + cmdParts.add rest + cmdParts.add outFlag + for a in @[targs, bargs]: + if a != "": cmdParts.add a + cmdParts.add b + + let cmd = cmdParts.join(" ") + if dryrun: + stderr.writeLine cmd + else: + if verbose: + termEcho styleBright, "cmd: ", ansiResetCode, cmd + let errCode = execCmd cmd + if errCode != 0: + termErrQuit "problems executing cmd " & cmd + +when isMainModule: + import cligen + zigExists() + + const + customMulti = "${doc}Usage:\n $command {SUBCMD} [sub-command options & parameters]\n\nsubcommands:\n$subcmds" + vsn = staticExec "git describe --tags --always HEAD" + + + if clCfg.useMulti == "": clCfg.useMulti = customMulti + if clCfg.helpAttr.len == 0: + clCfg.helpAttr = {"cmd": "\e[1;36m", "clDescrip": "", "clDflVal": "\e[33m", + "clOptKeys": "\e[32m", "clValType": "\e[31m", "args": "\e[3m"}.toTable + clCfg.helpAttrOff = {"cmd": "\e[m", "clDescrip": "\e[m", "clDflVal": "\e[m", + "clOptKeys": "\e[m", "clValType": "\e[m", "args": "\e[m"}.toTable + + var vsnCfg = clCfg + vsnCfg.version = vsn + + + dispatchMulti(["multi", cf = vsnCfg], [cc, help = { + "dryrun": "show command instead of executing", + "nimble": "use nimble as base command for compiling" + }], + [targets], + [release, + help = { + "target": "set target, may be repeated", + "bin": "set bin, may be repeated", + "dryrun": "show command instead of executing", + "format": "set format, see help above", + "nimble": "use nimble as base command for compiling", + "config-file": "path to config" + }, + short = {"verbose": 'V'} + ] + ) diff --git a/src/forge/config.nim b/src/forge/config.nim new file mode 100644 index 0000000..3908255 --- /dev/null +++ b/src/forge/config.nim @@ -0,0 +1,128 @@ +import std/[parsecfg, tables, os, strutils, strformat] + +type + Config* = object + targets*: OrderedTableRef[string, string] + bins*: OrderedTableRef[string, string] + outdir*: string + format*: string + name*: string + version*: string + nimble*: bool + +proc `$`*(c: Config): string = + var lines: seq[string] = @[] + lines.add "config =" + lines.add "| nimble " & $c.nimble + lines.add "| outdir " & c.outdir + lines.add "| format " & c.format + lines.add "| version " & c.version + lines.add "| targets:" + for target, args in c.targets: + lines.add "| " & target & (if args != "": "|" & args else: "") + lines.add "| bins:" + for bin, args in c.bins: + lines.add "| " & bin & (if args != "": "|" & args else: "") + + lines.join("\n") + +proc loadConfigFile*(f: string): Config = + let + dict = loadConfig(f) + base = dict.getOrDefault("") + + result.targets = newOrderedTable[string, string]() + result.bins = newOrderedTable[string, string]() + + result.nimble = base.hasKey("nimble") + result.outdir = base.getOrDefault("outdir") + result.name = base.getOrDefault("name") + result.version = base.getOrDefault("version") + result.format = base.getOrDefault("format") + if dict.hasKey("target"): + result.targets = dict.getOrDefault("target") + if dict.hasKey("bin"): + result.bins = dict.getOrDefault("bin") + +proc inferName(s: string, nimbleFile: string): string = + if s != "": + return s + elif nimbleFile != "": + return nimbleFile.rsplit(".", maxsplit = 1)[0] + +proc findNimbleFile(): string = + var candidates: seq[string] + for kind, path in walkDir(getCurrentDir(), relative = true): + case kind: + of pcFile, pcLinkToFile: + if path.endsWith(".nimble"): + candidates.add path + else: discard + + # nimble will probably prevent this, + # but not sure about atlas or bespoke builds + if candidates.len > 1: + echo "found multiple nimble files: " & candidates.join(", ") + echo "cannot infer name or version" + elif candidates.len == 1: + return candidates[0] + +proc inferVersion(s: string, nimbleFile: string): string = + if s != "": return s + + # TODO: catch io errors? + let nimbleCfg = loadConfig(nimbleFile) + return nimbleCfg.getSectionValue("", "version") + +proc inferBin(nimbleFile: string): string = + let + pkgName = nimbleFile.split(".")[0] + default = "src" / &"{pkgName}.nim" + backup = &"{pkgName}.nim" + + if default.fileExists(): return default + if backup.fileExists(): return backup + + +proc newConfig*( + targets: seq[string], + bins: seq[string], + outdir: string, + format: string, + name: string, + version: string, + nimble: bool, + configFile: string + ): Config = + + let nimbleFile = findNimbleFile() + + if configFile.fileExists: + result = loadConfigFile(configFile) + else: + # no seg faults here... + result.targets = newOrderedTable[string, string]() + result.bins = newOrderedTable[string, string]() + + result.nimble = result.nimble or nimble + + if result.outdir == "" or (result.outdir != "dist" and outdir != "dist"): + result.outdir = outdir + + if result.name == "": + result.name = inferName(name, nimbleFile) + if result.version == "": + result.version = inferVersion(version, nimbleFile) + if result.format == "": + if format != "": result.format = format + else: result.format = "${name}-v${version}-${target}" + + for t in targets: + result.targets[t] = "" + for b in bins: + result.bins[b] = "" + + if result.bins.len == 0 and nimbleFile != "": + let bin = inferBin(nimbleFile) + if bin != "": + result.bins[bin] = "" diff --git a/src/forge/term.nim b/src/forge/term.nim new file mode 100644 index 0000000..27e7285 --- /dev/null +++ b/src/forge/term.nim @@ -0,0 +1,17 @@ +import std/terminal + +# TODO support NO_COLOR for these +let prefix = ansiForegroundColorCode(fgMagenta, bright = true) & "forge" & + ansiResetCode & ansiForegroundColorCode(fgYellow) & " || " & ansiResetCode + +template termEcho*(args: varargs[untyped]) = + stderr.styledWriteLine(prefix, args) + +template termErr*(args: varargs[untyped]) = + stderr.styledWriteLine(prefix, fgRed, "error ", fgDefault, args) + +template termErrQuit*(args: varargs[untyped]) = + termErr(args) + quit 1 + + diff --git a/src/ccnz/utils.nim b/src/forge/utils.nim similarity index 54% rename from src/ccnz/utils.nim rename to src/forge/utils.nim index b6c81ac..1c1eace 100644 --- a/src/ccnz/utils.nim +++ b/src/forge/utils.nim @@ -1,8 +1,25 @@ -import std/[json, macros, math, os, osproc, strutils, terminal, sequtils] +import std/[json, macros, math, os, osproc, sequtils, strutils, strformat, terminal] -proc errQuit*(msg: varargs[string]) = - stderr.write msg.join("\n") & "\n" - quit 1 +import term + +proc columns*(items: seq[string]): string = + ## return a list of items as equally spaced columns + let + maxWidth = max(items.mapIt(it.len)) + nColumns = floor((terminalWidth() + 1) / (maxWidth + 1)).int + result = ( + items.mapIt(it.alignLeft(maxWidth + 1)) + ).distribute( + (items.len / nColumns).int + 1 + ).mapIt(it.join("")).join("\n") + +template parseArgs*(args: seq[string]): seq[string] = + if args.len == 0: + args + elif args[0] == "c": + args[1..^1] + else: + args # based on https://github.com/enthus1ast/zigcc template callZig*(zigCmd: string) = @@ -21,11 +38,33 @@ template callZig*(zigCmd: string) = close process quit exitCode + +type + Triplet* = object + cpu: string + os: string + libc: string + +proc `$`*(t: Triplet): string = &"{t.cpu}-{t.os}-{t.libc}" + +proc parseTriplet*(s: string, targets: seq[string]): Triplet = + if s notin targets: + termErr &"unknown target: {s}", "", "must be one of:" + stderr.writeLine targets.columns + quit 1 + + + let parts = s.split("-") + result.cpu = parts[0] + result.os = parts[1] + result.libc = parts[2] + proc zigExists*() = if (findExe "zig") == "": - errQuit "zig not found", - "ccnz requires a working installation of zig", - "see: https://ziglang.org/download/" + termErr "zig not found" + termErr " forge requires a working installation of zig" + termErr " see: https://ziglang.org/download/" + quit 1 proc zigTargets*(): seq[string] = let (output, _) = execCmdEx "zig targets" @@ -34,7 +73,7 @@ proc zigTargets*(): seq[string] = macro addFlag*(arg: untyped): untyped = let flag = "--" & arg.strVal & ":" - inferProc = newCall("infer" & arg.strVal, newIdentNode("target")) + inferProc = newCall("infer" & arg.strVal, newIdentNode("triplet")) quote do: if `flag` notin args: @@ -43,30 +82,24 @@ macro addFlag*(arg: untyped): untyped = result.add `flag` & selected -proc inferOs*(target: string): string = - if "windows" in target: - "Windows" - elif "macos" in target: - "MacOSX" - elif "linux" in target: - "Linux" - elif "wasm" in target: - "Linux" - else: - "" +proc inferOs*(t: Triplet): string = + case t.os: + of "windows", "linux": t.os.capitalizeAscii() + of "macos": "MacOSX" + of "wasm": "Linux" + else: "" -proc inferCpu*(target: string): string = +proc inferCpu*(t: Triplet): string = # Available options are: # i386, m68k, alpha, powerpc, powerpc64, powerpc64el, sparc, # vm, hppa, ia64, amd64, mips, mipsel, arm, arm64, js, # nimvm, avr, msp430, sparc64, mips64, mips64el, riscv32, # riscv64, esp, wasm32, e2k, loongarch64 - # - let candidate = target.split("-")[0] + # NOTE: I don't know what the _be eb means but if nim # can't handle them then maybe an error would be better result = - case candidate: + case t.cpu: of "x86_64": "amd64" of "aarch64", "aarch64_be": @@ -79,35 +112,21 @@ proc inferCpu*(target: string): string = "powerpc64le" # remain the same of "m68k", "mips64el", "mipsel", "mips", "powerpc", "powerpc64", "riscv64", - "sparc", "sparc64", "wasm32": - candidate + "sparc", "sparc64", "wasm32": t.cpu else: "" -# s390x-linux-gnu -# s390x-linux-musl -# sparc-linux-gnu -# sparc64-linux-gnu -# wasm32-freestanding-musl -# wasm32-wasi-musl -# x86_64-linux-gnu -# x86_64-linux-gnux32 -# x86_64-linux-musl -# x86_64-windows-gnu -# x86_64-macos-none -# x86_64-macos-none -# x86_64-macos-none -# -proc columns*(items: seq[string]): string = - ## return a list of items as equally spaced columns - let - maxWidth = max(items.mapIt(it.len)) - nColumns = floor((terminalWidth() + 1) / (maxWidth + 1)).int - result = ( - items.mapIt(it.alignLeft(maxWidth + 1)) - ).distribute( - (items.len / nColumns).int + 1 - ).mapIt(it.join("")).join("\n") - +proc formatDirName*(formatstr: string, name: string, version: string, + target: string): string = + var vsn = version + if ("v$version" in formatstr or "v${version}" in formatstr) and + vsn.startsWith("v"): + vsn = vsn[1..^1] + try: + result = formatstr % ["name", name, "version", vsn, "target", target] + except ValueError as e: + termErrQuit e.msg + if result == "": + termErrQuit &"error processing formatstr: {formatstr}" diff --git a/src/forgecc.nim b/src/forgecc.nim new file mode 100644 index 0000000..a2b88fd --- /dev/null +++ b/src/forgecc.nim @@ -0,0 +1,3 @@ +import forge/utils + +callZig("cc")