chore: ccnz -> forge

This commit is contained in:
Daylin Morgan 2023-09-05 16:47:37 -05:00
parent 9dc6353e8b
commit 4485f832f0
Signed by: daylin
GPG key ID: C1E52E7DD81DF79F
13 changed files with 447 additions and 174 deletions

13
.forge.cfg Normal file
View file

@ -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

View file

@ -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

21
LICENSE Normal file
View file

@ -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.

View file

@ -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
```

View file

@ -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

30
forge.nimble Normal file
View file

@ -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

View file

@ -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])

View file

@ -1,3 +0,0 @@
import ccnz/utils
callZig("cc")

155
src/forge.nim Normal file
View file

@ -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'}
]
)

128
src/forge/config.nim Normal file
View file

@ -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] = ""

17
src/forge/term.nim Normal file
View file

@ -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

View file

@ -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}"

3
src/forgecc.nim Normal file
View file

@ -0,0 +1,3 @@
import forge/utils
callZig("cc")