1049 lines
34 KiB
Nim
1049 lines
34 KiB
Nim
import hashes, macros, osproc, sets, strformat, strutils, tables
|
|
|
|
import os except findExe, sleep
|
|
|
|
import "."/[compat]
|
|
|
|
proc sanitizePath*(path: string, noQuote = false, sep = $DirSep): string =
|
|
result = path.multiReplace([("\\\\", sep), ("\\", sep), ("/", sep)])
|
|
if not noQuote:
|
|
result = result.quoteShell
|
|
|
|
proc sleep*(milsecs: int) =
|
|
## Sleep at compile time
|
|
let
|
|
cmd =
|
|
when defined(windows):
|
|
"cmd /c timeout "
|
|
else:
|
|
"sleep "
|
|
|
|
discard gorgeEx(cmd & $(milsecs / 1000))
|
|
|
|
proc getOsCacheDir(): string =
|
|
when defined(posix):
|
|
result = getEnv("XDG_CACHE_HOME", getHomeDir() / ".cache") / "nim"
|
|
else:
|
|
result = getHomeDir() / "nimcache"
|
|
|
|
proc getNimteropCacheDir(): string =
|
|
result = getOsCacheDir() / "nimterop"
|
|
|
|
proc getCurrentNimCompiler*(): string =
|
|
result = getCurrentCompilerExe()
|
|
when defined(nimsuggest):
|
|
result = result.replace("nimsuggest", "nim")
|
|
|
|
proc execAction*(cmd: string, retry = 0, die = true, cache = false,
|
|
cacheKey = ""): tuple[output: string, ret: int] =
|
|
## Execute an external command - supported at compile time
|
|
##
|
|
## Checks if command exits successfully before returning. If not, an
|
|
## error is raised. Always caches results to be used in nimsuggest or nimcheck
|
|
## mode.
|
|
##
|
|
## `retry` - number of times command should be retried before error
|
|
## `die = false` - return on errors
|
|
## `cache = true` - cache results unless cleared with -f
|
|
## `cacheKey` - key to create unique cache entry
|
|
var
|
|
ccmd = ""
|
|
when defined(Windows):
|
|
# Replace 'cd d:\abc' with 'd: && cd d:\abc`
|
|
var filteredCmd = cmd
|
|
if cmd.toLower().startsWith("cd"):
|
|
var
|
|
colonIndex = cmd.find(":")
|
|
driveLetter = cmd.substr(colonIndex-1, colonIndex)
|
|
if (driveLetter[0].isAlphaAscii() and
|
|
driveLetter[1] == ':' and
|
|
colonIndex == 4):
|
|
filteredCmd = &"{driveLetter} && {cmd}"
|
|
ccmd = "cmd /c " & filteredCmd
|
|
elif defined(posix):
|
|
ccmd = cmd
|
|
else:
|
|
doAssert false
|
|
|
|
when nimvm:
|
|
# Cache results for speedup if cache = true
|
|
# Else cache for preserving functionality in nimsuggest and nimcheck
|
|
let
|
|
hash = (ccmd & cacheKey).hash().abs()
|
|
cacheFile = getNimteropCacheDir() / "execCache" / "nimterop_" & $hash & ".txt"
|
|
|
|
when defined(nimsuggest) or defined(nimcheck):
|
|
# Load results from cache file if generated in previous run
|
|
if fileExists(cacheFile):
|
|
result.output = cacheFile.readFile()
|
|
elif die:
|
|
doAssert false, "Results not cached - run nim c/cpp at least once\n" & ccmd
|
|
else:
|
|
if cache and fileExists(cacheFile) and not compileOption("forceBuild"):
|
|
# Return from cache when requested
|
|
result.output = cacheFile.readFile()
|
|
else:
|
|
# Execute command and store results in cache
|
|
(result.output, result.ret) = gorgeEx(ccmd)
|
|
if result.ret == 0:
|
|
# mkdir for execCache dir (circular dependency)
|
|
let dir = cacheFile.parentDir()
|
|
if not dirExists(dir):
|
|
let flag = when not defined(Windows): "-p" else: ""
|
|
discard execAction(&"mkdir {flag} {dir.sanitizePath}")
|
|
cacheFile.writeFile(result.output)
|
|
else:
|
|
# Used by toast
|
|
(result.output, result.ret) = execCmdEx(ccmd)
|
|
|
|
# On failure, retry or die as requested
|
|
if result.ret != 0:
|
|
if retry > 0:
|
|
sleep(500)
|
|
result = execAction(cmd, retry = retry - 1, die, cache, cacheKey)
|
|
elif die:
|
|
doAssert false, "Command failed: " & $result.ret & "\ncmd: " & ccmd &
|
|
"\nresult:\n" & result.output
|
|
|
|
proc findExe*(exe: string): string =
|
|
## Find the specified executable using the `which`/`where` command - supported
|
|
## at compile time
|
|
var
|
|
cmd =
|
|
when defined(windows):
|
|
"where " & exe
|
|
else:
|
|
"which " & exe
|
|
|
|
(output, ret) = execAction(cmd, die = false)
|
|
|
|
if ret == 0:
|
|
return output.splitLines()[0].strip()
|
|
|
|
proc mkDir*(dir: string) =
|
|
## Create a directory at compile time
|
|
##
|
|
## The `os` module is not available at compile time so a few
|
|
## crucial helper functions are included with nimterop.
|
|
if not dirExists(dir):
|
|
let
|
|
flag = when not defined(Windows): "-p" else: ""
|
|
discard execAction(&"mkdir {flag} {dir.sanitizePath}", retry = 2)
|
|
|
|
proc cpFile*(source, dest: string, move=false) =
|
|
## Copy a file from `source` to `dest` at compile time
|
|
let
|
|
source = source.replace("/", $DirSep)
|
|
dest = dest.replace("/", $DirSep)
|
|
cmd =
|
|
when defined(Windows):
|
|
if move:
|
|
"move /y"
|
|
else:
|
|
"copy /y"
|
|
else:
|
|
if move:
|
|
"mv -f"
|
|
else:
|
|
"cp -f"
|
|
|
|
discard execAction(&"{cmd} {source.sanitizePath} {dest.sanitizePath}", retry = 2)
|
|
|
|
proc mvFile*(source, dest: string) =
|
|
## Move a file from `source` to `dest` at compile time
|
|
cpFile(source, dest, move=true)
|
|
|
|
proc rmFile*(source: string, dir = false) =
|
|
## Remove a file or pattern at compile time
|
|
let
|
|
source = source.replace("/", $DirSep)
|
|
cmd =
|
|
when defined(Windows):
|
|
if dir:
|
|
"rd /s/q"
|
|
else:
|
|
"del /s/q/f"
|
|
else:
|
|
"rm -rf"
|
|
exists =
|
|
if dir:
|
|
dirExists(source)
|
|
else:
|
|
fileExists(source)
|
|
|
|
if exists:
|
|
discard execAction(&"{cmd} {source.sanitizePath}", retry = 2)
|
|
|
|
proc rmDir*(dir: string) =
|
|
## Remove a directory or pattern at compile time
|
|
rmFile(dir, dir = true)
|
|
|
|
proc getProjectCacheDir*(name: string, forceClean = true): string =
|
|
## Get a cache directory where all nimterop artifacts can be stored
|
|
##
|
|
## Projects can use this location to download source code and build binaries
|
|
## that can be then accessed by multiple apps. This is created under the
|
|
## per-user Nim cache directory.
|
|
##
|
|
## Use `name` to specify the subdirectory name for a project.
|
|
##
|
|
## `forceClean` is enabled by default and effectively deletes the folder
|
|
## if Nim is compiled with the `-f` or `--forceBuild` flag. This allows
|
|
## any project to start out with a clean cache dir on a forced build.
|
|
##
|
|
## NOTE: avoid calling `getProjectCacheDir()` multiple times on the same
|
|
## `name` when `forceClean = true` else checked out source might get deleted
|
|
## at the wrong time during build.
|
|
##
|
|
## E.g.
|
|
## `nimgit2` downloads `libgit2` source so `name = "libgit2"`
|
|
##
|
|
## `nimarchive` downloads `libarchive`, `bzlib`, `liblzma` and `zlib` so
|
|
## `name = "nimarchive" / "libarchive"` for `libarchive`, etc.
|
|
result = getNimteropCacheDir() / name
|
|
|
|
if forceClean and compileOption("forceBuild"):
|
|
echo "# Removing " & result
|
|
rmDir(result)
|
|
|
|
proc extractZip*(zipfile, outdir: string) =
|
|
## Extract a zip file using `powershell` on Windows and `unzip` on other
|
|
## systems to the specified output directory
|
|
var cmd = "unzip -o $#"
|
|
if defined(Windows):
|
|
cmd = "powershell -nologo -noprofile -command \"& { Add-Type -A " &
|
|
"'System.IO.Compression.FileSystem'; " &
|
|
"[IO.Compression.ZipFile]::ExtractToDirectory('$#', '.'); }\""
|
|
|
|
echo "# Extracting " & zipfile
|
|
discard execAction(&"cd {outdir.sanitizePath} && {cmd % zipfile}")
|
|
|
|
proc extractTar*(tarfile, outdir: string) =
|
|
## Extract a tar file using `tar`, `7z` or `7za` to the specified output directory
|
|
var
|
|
cmd = ""
|
|
name = ""
|
|
|
|
if findExe("tar").len != 0:
|
|
let
|
|
ext = tarfile.splitFile().ext.toLowerAscii()
|
|
typ =
|
|
case ext
|
|
of ".gz", ".tgz": "z"
|
|
of ".xz": "J"
|
|
of ".bz2": "j"
|
|
else: ""
|
|
|
|
cmd = "tar xvf" & typ & " " & tarfile.sanitizePath
|
|
else:
|
|
for i in ["7z", "7za"]:
|
|
if findExe(i).len != 0:
|
|
cmd = i & " x $#" % tarfile.sanitizePath
|
|
|
|
name = tarfile.splitFile().name
|
|
if ".tar" in name.toLowerAscii():
|
|
cmd &= " && " & i & " x $#" % name.sanitizePath
|
|
|
|
break
|
|
|
|
doAssert cmd.len != 0, "No extraction tool - tar, 7z, 7za - available for " & tarfile.sanitizePath
|
|
|
|
echo "# Extracting " & tarfile
|
|
discard execAction(&"cd {outdir.sanitizePath} && {cmd}")
|
|
if name.len != 0:
|
|
rmFile(outdir / name)
|
|
|
|
proc downloadUrl*(url, outdir: string) =
|
|
## Download a file using `curl` or `wget` (or `powershell` on Windows) to the specified directory
|
|
##
|
|
## If an archive file, it is automatically extracted after download.
|
|
let
|
|
file = url.extractFilename()
|
|
ext = file.splitFile().ext.toLowerAscii()
|
|
archives = @[".zip", ".xz", ".gz", ".bz2", ".tgz", ".tar"]
|
|
|
|
if not (ext in archives and fileExists(outdir/file)):
|
|
echo "# Downloading " & file
|
|
mkDir(outdir)
|
|
var cmd = findExe("curl")
|
|
if cmd.len != 0:
|
|
cmd &= " -Lk $# -o $#"
|
|
else:
|
|
cmd = findExe("wget")
|
|
if cmd.len != 0:
|
|
cmd &= " $# -O $#"
|
|
elif defined(Windows):
|
|
cmd = "powershell [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; wget $# -OutFile $#"
|
|
else:
|
|
doAssert false, "No download tool available - curl, wget"
|
|
discard execAction(cmd % [url, (outdir/file).sanitizePath], retry = 1)
|
|
|
|
if ext == ".zip":
|
|
extractZip(file, outdir)
|
|
elif ext in archives:
|
|
extractTar(file, outdir)
|
|
|
|
proc gitReset*(outdir: string) =
|
|
## Hard reset the git repository at the specified directory
|
|
echo "# Resetting " & outdir
|
|
|
|
let cmd = &"cd {outdir.sanitizePath} && git reset --hard"
|
|
while execAction(cmd).output.contains("Permission denied"):
|
|
sleep(1000)
|
|
echo "# Retrying ..."
|
|
|
|
proc gitCheckout*(file, outdir: string) =
|
|
## Checkout the specified `file` in the git repository at `outdir`
|
|
##
|
|
## This effectively resets all changes in the file and can be
|
|
## used to undo any changes that were made to source files to enable
|
|
## successful wrapping with `cImport()` or `c2nImport()`.
|
|
echo "# Resetting " & file
|
|
let file2 = file.relativePath outdir
|
|
let cmd = &"cd {outdir.sanitizePath} && git checkout {file2.sanitizePath}"
|
|
while execAction(cmd).output.contains("Permission denied"):
|
|
sleep(500)
|
|
echo "# Retrying ..."
|
|
|
|
proc gitPull*(url: string, outdir = "", plist = "", checkout = "") =
|
|
## Pull the specified git repository to the output directory
|
|
##
|
|
## `plist` is the list of specific files and directories or wildcards
|
|
## to sparsely checkout. Multiple values can be specified one entry per
|
|
## line. It is optional and if omitted, the entire repository will be
|
|
## checked out.
|
|
##
|
|
## `checkout` is the git tag, branch or commit hash to checkout once
|
|
## the repository is downloaded. This allows for pinning to a specific
|
|
## version of the code.
|
|
if dirExists(outdir/".git"):
|
|
gitReset(outdir)
|
|
return
|
|
|
|
let
|
|
outdirQ = outdir.sanitizePath
|
|
|
|
mkDir(outdir)
|
|
|
|
echo "# Setting up Git repo: " & url
|
|
discard execAction(&"cd {outdirQ} && git init .")
|
|
discard execAction(&"cd {outdirQ} && git remote add origin {url}")
|
|
|
|
if plist.len != 0:
|
|
# If a specific list of files is required, create a sparse checkout
|
|
# file for git in its config directory
|
|
let sparsefile = outdir / ".git/info/sparse-checkout"
|
|
|
|
discard execAction(&"cd {outdirQ} && git config core.sparsecheckout true")
|
|
writeFile(sparsefile, plist)
|
|
|
|
if checkout.len != 0:
|
|
echo "# Checking out " & checkout
|
|
discard execAction(&"cd {outdirQ} && git fetch", retry = 1)
|
|
discard execAction(&"cd {outdirQ} && git checkout {checkout}")
|
|
else:
|
|
echo "# Pulling repository"
|
|
discard execAction(&"cd {outdirQ} && git pull --depth=1 origin master", retry = 1)
|
|
|
|
proc findFile*(file: string, dir: string, recurse = true, first = false, regex = false): string =
|
|
## Find the file in the specified directory
|
|
##
|
|
## `file` is a regular expression if `regex` is true
|
|
##
|
|
## Turn off recursive search with `recurse` and stop on first match with
|
|
## `first`. Without it, the shortest match is returned.
|
|
var
|
|
cmd =
|
|
when defined(windows):
|
|
"nimgrep --filenames --oneline --nocolor $1 \"$2\" $3"
|
|
elif defined(linux):
|
|
"find $3 $1 -regextype egrep -regex $2"
|
|
elif defined(osx):
|
|
"find -E $3 $1 -regex $2"
|
|
|
|
recursive = ""
|
|
|
|
if recurse:
|
|
when defined(windows):
|
|
recursive = "--recursive"
|
|
else:
|
|
when not defined(windows):
|
|
recursive = "-maxdepth 1"
|
|
|
|
var
|
|
dir = dir
|
|
file = file
|
|
if not recurse:
|
|
let
|
|
pdir = file.parentDir()
|
|
if pdir.len != 0:
|
|
dir = dir / pdir
|
|
|
|
file = file.extractFilename
|
|
|
|
cmd = cmd % [recursive, (".*[\\\\/]" & file & "$").quoteShell, dir.sanitizePath]
|
|
|
|
let
|
|
(files, ret) = execAction(cmd, die = false)
|
|
if ret == 0:
|
|
for line in files.splitLines():
|
|
let f =
|
|
when defined(windows):
|
|
if ": " in line:
|
|
line.split(": ", maxsplit = 1)[1]
|
|
else:
|
|
""
|
|
else:
|
|
line
|
|
|
|
if (f.len != 0 and (result.len == 0 or result.len > f.len)):
|
|
result = f
|
|
if first: break
|
|
|
|
proc flagBuild*(base: string, flags: openArray[string]): string =
|
|
## Simple helper proc to generate flags for `configure`, `cmake`, etc.
|
|
##
|
|
## Every entry in `flags` is replaced into the `base` string and
|
|
## concatenated to the result.
|
|
##
|
|
## E.g.
|
|
## `base = "--disable-$#"`
|
|
## `flags = @["one", "two"]`
|
|
##
|
|
## `flagBuild(base, flags) => " --disable-one --disable-two"`
|
|
for i in flags:
|
|
result &= " " & base % i
|
|
|
|
proc linkLibs*(names: openArray[string], staticLink = true): string =
|
|
## Create linker flags for specified libraries
|
|
##
|
|
## Prepends `lib` to the name so you only need `ssl` for `libssl`.
|
|
var
|
|
stat = if staticLink: "--static" else: ""
|
|
resSet: OrderedSet[string]
|
|
resSet.init()
|
|
|
|
for name in names:
|
|
let
|
|
cmd = &"pkg-config --libs --silence-errors {stat} lib{name}"
|
|
(libs, _) = execAction(cmd, die = false)
|
|
for lib in libs.split(" "):
|
|
resSet.incl lib
|
|
|
|
if staticLink:
|
|
resSet.incl "--static"
|
|
|
|
for res in resSet:
|
|
result &= " " & res
|
|
|
|
proc configure*(path, check: string, flags = "") =
|
|
## Run the GNU `configure` command to generate all Makefiles or other
|
|
## build scripts in the specified path
|
|
##
|
|
## If a `configure` script is not present and an `autogen.sh` script
|
|
## is present, it will be run before attempting `configure`.
|
|
##
|
|
## Next, if `configure.ac` or `configure.in` exist, `autoreconf` will
|
|
## be executed.
|
|
##
|
|
## `check` is a file that will be generated by the `configure` command.
|
|
## This is required to prevent configure from running on every build. It
|
|
## is relative to the `path` and should not be an absolute path.
|
|
##
|
|
## `flags` are any flags that should be passed to the `configure` command.
|
|
if (path / check).fileExists():
|
|
return
|
|
|
|
echo "# Configuring " & path
|
|
|
|
if not fileExists(path / "configure"):
|
|
for i in @["autogen.sh", "build" / "autogen.sh"]:
|
|
if fileExists(path / i):
|
|
echo "# Running autogen.sh"
|
|
|
|
echo execAction(
|
|
&"cd {(path / i).parentDir().sanitizePath} && bash autogen.sh").output
|
|
|
|
break
|
|
|
|
if not fileExists(path / "configure"):
|
|
for i in @["configure.ac", "configure.in"]:
|
|
if fileExists(path / i):
|
|
echo "# Running autoreconf"
|
|
|
|
echo execAction(&"cd {path.sanitizePath} && autoreconf -fi").output
|
|
|
|
break
|
|
|
|
if fileExists(path / "configure"):
|
|
echo "# Running configure " & flags
|
|
|
|
var
|
|
cmd = &"cd {path.sanitizePath} && bash configure"
|
|
if flags.len != 0:
|
|
cmd &= &" {flags}"
|
|
|
|
echo execAction(cmd).output
|
|
|
|
doAssert (path / check).fileExists(), "# Configure failed"
|
|
|
|
proc getCmakePropertyStr(name, property, value: string): string =
|
|
&"\nset_target_properties({name} PROPERTIES {property} \"{value}\")\n"
|
|
|
|
proc getCmakeIncludePath*(paths: openArray[string]): string =
|
|
## Create a `cmake` flag to specify custom include paths
|
|
##
|
|
## Result can be included in the `flag` parameter for `cmake()` or
|
|
## the `cmakeFlags` parameter for `getHeader()`.
|
|
for path in paths:
|
|
result &= path & ";"
|
|
result = " -DCMAKE_INCLUDE_PATH=" & result[0 .. ^2].sanitizePath(sep = "/")
|
|
|
|
proc setCmakeProperty*(outdir, name, property, value: string) =
|
|
## Set a `cmake` property in `outdir / CMakeLists.txt` - usable in the `xxxPreBuild` hook
|
|
## for `getHeader()`
|
|
##
|
|
## `set_target_properties(name PROPERTIES property "value")`
|
|
let
|
|
cm = outdir / "CMakeLists.txt"
|
|
if cm.fileExists():
|
|
cm.writeFile(
|
|
cm.readFile() & getCmakePropertyStr(name, property, value)
|
|
)
|
|
|
|
proc setCmakeLibName*(outdir, name, prefix = "", oname = "", suffix = "") =
|
|
## Set a `cmake` property in `outdir / CMakeLists.txt` to specify a custom library output
|
|
## name - usable in the `xxxPreBuild` hook for `getHeader()`
|
|
##
|
|
## `prefix` is typically `lib`
|
|
## `oname` is the library name
|
|
## `suffix` is typically `.a`
|
|
##
|
|
## Sometimes, `cmake` generates non-standard library names - e.g. zlib compiles to
|
|
## `libzlibstatic.a` on Windows. This proc can help rename it to `libzlib.a` so that `getHeader()`
|
|
## can find it after the library is compiled.
|
|
##
|
|
## ```
|
|
## set_target_properties(name PROPERTIES PREFIX "prefix")
|
|
## set_target_properties(name PROPERTIES OUTPUT_NAME "oname")
|
|
## set_target_properties(name PROPERTIES SUFFIX "suffix")
|
|
## ```
|
|
let
|
|
cm = outdir / "CMakeLists.txt"
|
|
if cm.fileExists():
|
|
var
|
|
str = ""
|
|
if prefix.len != 0:
|
|
str &= getCmakePropertyStr(name, "PREFIX", prefix)
|
|
if oname.len != 0:
|
|
str &= getCmakePropertyStr(name, "OUTPUT_NAME", oname)
|
|
if suffix.len != 0:
|
|
str &= getCmakePropertyStr(name, "SUFFIX", suffix)
|
|
if str.len != 0:
|
|
cm.writeFile(cm.readFile() & str)
|
|
|
|
proc setCmakePositionIndependentCode*(outdir: string) =
|
|
## Set a `cmake` directive to create libraries with -fPIC enabled
|
|
let
|
|
cm = outdir / "CMakeLists.txt"
|
|
if cm.fileExists():
|
|
let
|
|
pic = "set(CMAKE_POSITION_INDEPENDENT_CODE ON)"
|
|
cmd = cm.readFile()
|
|
if not cmd.contains(pic):
|
|
cm.writeFile(
|
|
pic & "\n" & cmd
|
|
)
|
|
|
|
proc cmake*(path, check, flags: string) =
|
|
## Run the `cmake` command to generate all Makefiles or other
|
|
## build scripts in the specified path
|
|
##
|
|
## `path` will be created since typically `cmake` is run in an
|
|
## empty directory.
|
|
##
|
|
## `check` is a file that will be generated by the `cmake` command.
|
|
## This is required to prevent `cmake` from running on every build. It
|
|
## is relative to the `path` and should not be an absolute path.
|
|
##
|
|
## `flags` are any flags that should be passed to the `cmake` command.
|
|
## Unlike `configure`, it is required since typically it will be the
|
|
## path to the repository, typically `..` when `path` is a subdir.
|
|
if (path / check).fileExists():
|
|
return
|
|
|
|
echo "# Running cmake " & flags
|
|
echo "# Path: " & path
|
|
|
|
mkDir(path)
|
|
|
|
var
|
|
cmd = &"cd {path.sanitizePath} && cmake {flags}"
|
|
|
|
echo execAction(cmd).output
|
|
|
|
doAssert (path / check).fileExists(), "# cmake failed"
|
|
|
|
proc make*(path, check: string, flags = "", regex = false) =
|
|
## Run the `make` command to build all binaries in the specified path
|
|
##
|
|
## `check` is a file that will be generated by the `make` command.
|
|
## This is required to prevent `make` from running on every build. It
|
|
## is relative to the `path` and should not be an absolute path.
|
|
##
|
|
## `flags` are any flags that should be passed to the `make` command.
|
|
##
|
|
## `regex` can be set to true if `check` is a regular expression.
|
|
##
|
|
## If `make.exe` is missing and `mingw32-make.exe` is available, it will
|
|
## be copied over to make.exe in the same location.
|
|
if findFile(check, path, regex = regex).len != 0:
|
|
return
|
|
|
|
echo "# Running make " & flags
|
|
echo "# Path: " & path
|
|
|
|
var
|
|
cmd = findExe("make")
|
|
|
|
if cmd.len == 0:
|
|
cmd = findExe("mingw32-make")
|
|
if cmd.len != 0:
|
|
cpFile(cmd, cmd.replace("mingw32-make", "make"))
|
|
doAssert cmd.len != 0, "Make not found"
|
|
|
|
cmd = &"cd {path.sanitizePath} && make"
|
|
if flags.len != 0:
|
|
cmd &= &" {flags}"
|
|
|
|
echo execAction(cmd).output
|
|
|
|
doAssert findFile(check, path, regex = regex).len != 0, "# make failed"
|
|
|
|
proc getCompiler*(): string =
|
|
var
|
|
compiler =
|
|
when defined(gcc):
|
|
"gcc"
|
|
elif defined(clang):
|
|
"clang"
|
|
else:
|
|
doAssert false, "Nimterop only supports gcc and clang at this time"
|
|
|
|
result = getEnv("CC", compiler)
|
|
|
|
proc getGccPaths*(mode = "c"): seq[string] =
|
|
var
|
|
nul = when defined(Windows): "nul" else: "/dev/null"
|
|
mmode = if mode == "cpp": "c++" else: mode
|
|
inc = false
|
|
|
|
(outp, _) = execAction(&"""{getCompiler()} -Wp,-v -x{mmode} {nul}""", die = false)
|
|
|
|
for line in outp.splitLines():
|
|
if "#include <...> search starts here" in line:
|
|
inc = true
|
|
continue
|
|
elif "End of search list" in line:
|
|
break
|
|
if inc:
|
|
var
|
|
path = line.strip().myNormalizedPath()
|
|
if path notin result:
|
|
result.add path
|
|
|
|
when defined(osx):
|
|
result.add(execAction("xcrun --show-sdk-path").output.strip() & "/usr/include")
|
|
|
|
proc getGccLibPaths*(mode = "c"): seq[string] =
|
|
var
|
|
nul = when defined(Windows): "nul" else: "/dev/null"
|
|
mmode = if mode == "cpp": "c++" else: mode
|
|
linker = when defined(OSX): "-Xlinker" else: ""
|
|
|
|
(outp, _) = execAction(&"""{getCompiler()} {linker} -v -x{mmode} {nul}""", die = false)
|
|
|
|
for line in outp.splitLines():
|
|
if "LIBRARY_PATH=" in line:
|
|
for path in line[13 .. ^1].split(PathSep):
|
|
var
|
|
path = path.strip().myNormalizedPath()
|
|
if path notin result:
|
|
result.add path
|
|
break
|
|
elif '\t' in line:
|
|
var
|
|
path = line.strip().myNormalizedPath()
|
|
if path notin result:
|
|
result.add path
|
|
|
|
when defined(osx):
|
|
result.add "/usr/lib"
|
|
|
|
proc getStdPath(header: string): string =
|
|
for inc in getGccPaths():
|
|
result = findFile(header, inc, recurse = false, first = true)
|
|
if result.len != 0:
|
|
break
|
|
|
|
proc getStdLibPath(lname: string): string =
|
|
for lib in getGccLibPaths():
|
|
result = findFile(lname, lib, recurse = false, first = true, regex = true)
|
|
if result.len != 0:
|
|
break
|
|
|
|
proc getGitPath(header, url, outdir, version: string): string =
|
|
doAssert url.len != 0, "No git url setup for " & header
|
|
doAssert findExe("git").len != 0, "git executable missing"
|
|
|
|
gitPull(url, outdir, checkout = version)
|
|
|
|
result = findFile(header, outdir)
|
|
|
|
proc getDlPath(header, url, outdir, version: string): string =
|
|
doAssert url.len != 0, "No download url setup for " & header
|
|
|
|
var
|
|
dlurl = url
|
|
if "$#" in url or "$1" in url:
|
|
doAssert version.len != 0, "Need version for download url"
|
|
dlurl = url % version
|
|
else:
|
|
doAssert version.len == 0, "Download url does not contain version"
|
|
|
|
downloadUrl(dlurl, outdir)
|
|
|
|
var
|
|
dirname = ""
|
|
for kind, path in walkDir(outdir, relative = true):
|
|
if kind == pcFile and path != dlurl.extractFilename():
|
|
dirname = ""
|
|
break
|
|
elif kind == pcDir:
|
|
if dirname.len == 0:
|
|
dirname = path
|
|
else:
|
|
dirname = ""
|
|
break
|
|
|
|
if dirname.len != 0:
|
|
for kind, path in walkDir(outdir / dirname, relative = true):
|
|
mvFile(outdir / dirname / path, outdir / path)
|
|
|
|
result = findFile(header, outdir)
|
|
|
|
proc getLocalPath(header, outdir: string): string =
|
|
if outdir.len != 0:
|
|
result = findFile(header, outdir)
|
|
|
|
proc getNumProcs(): string =
|
|
when defined(windows):
|
|
getEnv("NUMBER_OF_PROCESSORS").strip()
|
|
elif defined(linux):
|
|
execAction("nproc").output.strip()
|
|
elif defined(macosx):
|
|
execAction("sysctl -n hw.ncpu").output.strip()
|
|
else:
|
|
"1"
|
|
|
|
proc buildLibrary(lname, outdir, conFlags, cmakeFlags, makeFlags: string): string =
|
|
var
|
|
conDeps = false
|
|
conDepStr = ""
|
|
cmakeDeps = false
|
|
cmakeDepStr = ""
|
|
lpath = findFile(lname, outdir, regex = true)
|
|
makeFlagsProc = &"-j {getNumProcs()} {makeFlags}"
|
|
made = false
|
|
makePath = outdir
|
|
|
|
if lpath.len != 0:
|
|
return lpath
|
|
|
|
if not fileExists(outdir / "Makefile"):
|
|
if fileExists(outdir / "CMakeLists.txt"):
|
|
if findExe("cmake").len != 0:
|
|
var
|
|
gen = ""
|
|
when defined(windows):
|
|
if findExe("sh").len != 0:
|
|
let
|
|
uname = execAction("sh -c uname -a").output.toLowerAscii()
|
|
if uname.contains("msys"):
|
|
gen = "MSYS Makefiles".quoteShell
|
|
elif uname.contains("mingw"):
|
|
gen = "MinGW Makefiles".quoteShell & " -DCMAKE_SH=\"CMAKE_SH-NOTFOUND\""
|
|
else:
|
|
echo "Unsupported system: " & uname
|
|
else:
|
|
gen = "MinGW Makefiles".quoteShell
|
|
else:
|
|
gen = "Unix Makefiles".quoteShell
|
|
if findExe("ccache").len != 0:
|
|
gen &= " -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache"
|
|
makePath = outdir / "buildcache"
|
|
cmake(makePath, "Makefile", &".. -G {gen} {cmakeFlags}")
|
|
cmakeDeps = true
|
|
else:
|
|
cmakeDepStr &= "cmake executable missing"
|
|
|
|
if not cmakeDeps:
|
|
if findExe("bash").len != 0:
|
|
for file in @["configure", "configure.ac", "configure.in", "autogen.sh", "build/autogen.sh"]:
|
|
if fileExists(outdir / file):
|
|
configure(outdir, "Makefile", conFlags)
|
|
conDeps = true
|
|
|
|
break
|
|
else:
|
|
conDepStr &= "bash executable missing"
|
|
|
|
if fileExists(makePath / "Makefile"):
|
|
make(makePath, lname, makeFlagsProc, regex = true)
|
|
made = true
|
|
|
|
var
|
|
error = ""
|
|
if not cmakeDeps and cmakeDepStr.len != 0:
|
|
error &= &"cmake capable but {cmakeDepStr}\n"
|
|
if not conDeps and conDepStr.len != 0:
|
|
error &= &"configure capable but {conDepStr}\n"
|
|
if error.len == 0:
|
|
error = "No build files found in " & outdir
|
|
doAssert cmakeDeps or conDeps or made, &"\n# Build configuration failed - {error}\n"
|
|
|
|
result = findFile(lname, outdir, regex = true)
|
|
|
|
proc getDynlibExt(): string =
|
|
when defined(windows):
|
|
result = ".dll"
|
|
elif defined(linux):
|
|
result = ".so[0-9.]*"
|
|
elif defined(macosx):
|
|
result = ".dylib[0-9.]*"
|
|
|
|
var
|
|
gDefines {.compileTime.} = initTable[string, string]()
|
|
|
|
macro setDefines*(defs: static openArray[string]): untyped =
|
|
## Specify `-d:xxx` values in code instead of having to rely on the command
|
|
## line or `cfg` or `nims` files.
|
|
##
|
|
## At this time, Nim does not allow creation of `-d:xxx` defines in code. In
|
|
## addition, Nim only loads config files for the module being compiled but not
|
|
## for imported packages. This becomes a challenge when wanting to ship a wrapper
|
|
## library that wants to control `getHeader()` for an underlying package.
|
|
##
|
|
## E.g. nimarchive wanting to set `-d:lzmaStatic`
|
|
##
|
|
## The consumer of nimarchive would need to set such defines as part of their
|
|
## project, making it inconvenient.
|
|
##
|
|
## By calling this proc with the defines preferred before importing such a module,
|
|
## the caller can set the behavior in code instead.
|
|
##
|
|
## .. code-block:: nim
|
|
##
|
|
## setDefines(@["lzmaStatic", "lzmaDL", "lzmaSetVer=5.2.4"])
|
|
##
|
|
## import lzma
|
|
for def in defs:
|
|
let
|
|
nv = def.strip().split("=", maxsplit = 1)
|
|
if nv.len != 0:
|
|
let
|
|
n = nv[0]
|
|
v =
|
|
if nv.len == 2:
|
|
nv[1]
|
|
else:
|
|
""
|
|
gDefines[n] = v
|
|
|
|
macro clearDefines*(): untyped =
|
|
## Clear all defines set using `setDefines()`.
|
|
gDefines.clear()
|
|
|
|
macro isDefined*(def: untyped): untyped =
|
|
## Check if `-d:xxx` is set globally or via `setDefines()`
|
|
let
|
|
sdef = gDefines.hasKey(def.strVal())
|
|
result = newNimNode(nnkStmtList)
|
|
result.add(quote do:
|
|
when defined(`def`) or `sdef` != 0:
|
|
true
|
|
else:
|
|
false
|
|
)
|
|
|
|
macro getHeader*(header: static[string], giturl: static[string] = "", dlurl: static[string] = "", outdir: static[string] = "",
|
|
conFlags: static[string] = "", cmakeFlags: static[string] = "", makeFlags: static[string] = "",
|
|
altNames: static[string] = ""): untyped =
|
|
## Get the path to a header file for wrapping with
|
|
## `cImport() <cimport.html#cImport.m%2C%2Cstring%2Cstring%2Cstring>`_ or
|
|
## `c2nImport() <cimport.html#c2nImport.m%2C%2Cstring%2Cstring%2Cstring>`_.
|
|
##
|
|
## This proc checks `-d:xxx` defines based on the header name (e.g. lzma from lzma.h),
|
|
## and accordingly employs different ways to obtain the source.
|
|
##
|
|
## `-d:xxxStd` - search standard system paths. E.g. `/usr/include` and `/usr/lib` on Linux
|
|
## `-d:xxxGit` - clone source from a git repo specified in `giturl`
|
|
## `-d:xxxDL` - download source from `dlurl` and extract if required
|
|
##
|
|
## This allows a single wrapper to be used in different ways depending on the user's needs.
|
|
## If no `-d:xxx` defines are specified, `outdir` will be searched for the header as is.
|
|
##
|
|
## If multiple `-d:xxx` defines are specified, precedence is `Std` and then `Git` or `DL`.
|
|
## This allows using a system installed library if available before falling back to manual
|
|
## building.
|
|
##
|
|
## `-d:xxxSetVer=x.y.z` can be used to specify which version to use. It is used as a tag
|
|
## name for Git whereas for DL, it replaces `$1` in the URL defined.
|
|
##
|
|
## All defines can also be set in code using `setDefines()`.
|
|
##
|
|
## The library is then configured (with `cmake` or `autotools` if possible) and built
|
|
## using `make`, unless using `-d:xxxStd` which presumes that the system package
|
|
## manager was used to install prebuilt headers and binaries.
|
|
##
|
|
## The header path is stored in `const xxxPath` and can be used in a `cImport()` call
|
|
## in the calling wrapper. The dynamic library path is stored in `const xxxLPath` and can
|
|
## be used for the `dynlib` parameter (within quotes) or with `{.passL.}`.
|
|
##
|
|
## `-d:xxxStatic` can be specified to statically link with the library instead. This
|
|
## will automatically add a `{.passL.}` call to the static library for convenience.
|
|
##
|
|
## `conFlags`, `cmakeFlags` and `makeFlags` allow sending custom parameters to `configure`,
|
|
## `cmake` and `make` in case additional configuration is required as part of the build process.
|
|
##
|
|
## `altNames` is a list of alternate names for the library - e.g. zlib uses `zlib.h` for the header but
|
|
## the typical lib name is `libz.so` and not `libzlib.so`. However, it is libzlib.dll on Windows if built
|
|
## with cmake. In this case, `altNames = "z,zlib"`. Comma separate for multiple alternate names without
|
|
## spaces.
|
|
##
|
|
## The original header name is not included by default if `altNames` is set since it could cause the
|
|
## wrong lib to be selected. E.g. `SDL2/SDL.h` could pick `libSDL.so` even if `altNames = "SDL2"`.
|
|
## Explicitly include it in `altNames` like the `zlib` example when required.
|
|
##
|
|
## `xxxPreBuild` is a hook that is called after the source code is pulled from Git or downloaded but
|
|
## before the library is built. This might be needed if some initial prep needs to be done before
|
|
## compilation. A few values are provided to the hook to help provide context:
|
|
##
|
|
## `outdir` is the same `outdir` passed in and `header` is the discovered header path in the
|
|
## downloaded source code.
|
|
##
|
|
## Simply define `proc xxxPreBuild(outdir, header: string)` in the wrapper and it will get called
|
|
## prior to the build process.
|
|
var
|
|
name = header.extractFilename().split(".")[0]
|
|
|
|
# -d:xxx for this header
|
|
stdStr = name & "Std"
|
|
gitStr = name & "Git"
|
|
dlStr = name & "DL"
|
|
|
|
staticStr = name & "Static"
|
|
verStr = name & "SetVer"
|
|
|
|
# Ident nodes of the -d:xxx to check in when statements
|
|
nameStd = newIdentNode(stdStr)
|
|
nameGit = newIdentNode(gitStr)
|
|
nameDL = newIdentNode(dlStr)
|
|
|
|
nameStatic = newIdentNode(staticStr)
|
|
|
|
# Consts to generate
|
|
path = newIdentNode(name & "Path")
|
|
lpath = newIdentNode(name & "LPath")
|
|
version = newIdentNode(verStr)
|
|
lname = newIdentNode(name & "LName")
|
|
preBuild = newIdentNode(name & "PreBuild")
|
|
|
|
# Regex for library search
|
|
lre = "(lib)?$1[_]?(static)?[0-9.\\-]*\\"
|
|
|
|
# If -d:xxx set with setDefines()
|
|
stdVal = gDefines.hasKey(stdStr)
|
|
gitVal = gDefines.hasKey(gitStr)
|
|
dlVal = gDefines.hasKey(dlStr)
|
|
staticVal = gDefines.hasKey(staticStr)
|
|
verVal =
|
|
if gDefines.hasKey(verStr):
|
|
gDefines[verStr]
|
|
else:
|
|
""
|
|
|
|
# Use alternate library names if specified for regex search
|
|
if altNames.len != 0:
|
|
lre = lre % ("(" & altNames.replace(",", "|") & ")")
|
|
else:
|
|
lre = lre % name
|
|
|
|
result = newNimNode(nnkStmtList)
|
|
result.add(quote do:
|
|
# Need to check -d:xxx or setDefines()
|
|
const
|
|
`nameStd`* = when defined(`nameStd`): true else: `stdVal` == 1
|
|
`nameGit`* = when defined(`nameGit`): true else: `gitVal` == 1
|
|
`nameDL`* = when defined(`nameDL`): true else: `dlVal` == 1
|
|
`nameStatic`* = when defined(`nameStatic`): true else: `staticVal` == 1
|
|
|
|
# Search for header in outdir (after retrieving code) depending on -d:xxx mode
|
|
proc getPath(header, giturl, dlurl, outdir, version: string): string =
|
|
when `nameGit`:
|
|
getGitPath(header, giturl, outdir, version)
|
|
elif `nameDL`:
|
|
getDlPath(header, dlurl, outdir, version)
|
|
else:
|
|
getLocalPath(header, outdir)
|
|
|
|
const
|
|
`version`* {.strdefine.} = `verVal`
|
|
`lname` =
|
|
when `nameStatic`:
|
|
`lre` & ".a"
|
|
else:
|
|
`lre` & getDynlibExt()
|
|
|
|
# Look in standard path if requested by user
|
|
stdPath =
|
|
when `nameStd`: getStdPath(`header`) else: ""
|
|
stdLPath =
|
|
when `nameStd`: getStdLibPath(`lname`) else: ""
|
|
|
|
# Look elsewhere if requested while prioritizing standard paths
|
|
prePath =
|
|
when stdPath.len != 0 and stdLPath.len != 0:
|
|
stdPath
|
|
else:
|
|
getPath(`header`, `giturl`, `dlurl`, `outdir`, `version`)
|
|
|
|
# Run preBuild hook before building library if not standard
|
|
when (prePath != stdPath or prePath.len == 0) and declared(`preBuild`):
|
|
static:
|
|
`preBuild`(`outdir`, prePath)
|
|
|
|
const
|
|
# Library binary path - build if not standard
|
|
`lpath`* =
|
|
when stdPath.len != 0 and stdLPath.len != 0:
|
|
stdLPath
|
|
else:
|
|
buildLibrary(`lname`, `outdir`, `conFlags`, `cmakeFlags`, `makeFlags`)
|
|
|
|
# Header path - search again in case header is generated in build
|
|
`path`* =
|
|
if prePath.len != 0:
|
|
prePath
|
|
else:
|
|
getPath(`header`, `giturl`, `dlurl`, `outdir`, `version`)
|
|
|
|
static:
|
|
doAssert `path`.len != 0, "\nHeader " & `header` & " not found - " & "missing/empty outdir or -d:$1Std -d:$1Git or -d:$1DL not specified" % `name`
|
|
doAssert `lpath`.len != 0, "\nLibrary " & `lname` & " not found"
|
|
echo "# Including library " & `lpath`
|
|
|
|
# Automatically link with static libary
|
|
when `nameStatic`:
|
|
{.passL: `lpath`.}
|
|
)
|