Fix #151 and fix #153 - detect nim.cfg

This commit is contained in:
Ganesh Viswanathan 2020-04-27 22:38:49 -05:00
commit 33d77f82e8
6 changed files with 238 additions and 34 deletions

View file

@ -12,7 +12,7 @@ The nimterop wrapping functionality is still limited to C but is constantly expa
Nimterop has seen some adoption within the community and the simplicity and success of this approach justifies additional investment of time and effort. Regardless, the goal is to make interop seamless so nimterop will focus on wrapping headers and not the outright conversion of C/C++ implementation.
### Installation
## Installation
Nimterop can be installed via [Nimble](https://github.com/nim-lang/nimble):
@ -28,11 +28,11 @@ nimble build
This will download and install nimterop in the standard Nimble package location, typically `~/.nimble`. Once installed, it can be imported into any Nim program.
### Usage
## Usage
Detailed documentation can be found [here](https://nimterop.github.io/nimterop/theindex.html). Also, check out the [wiki](https://github.com/nimterop/nimterop/wiki/Wrappers) for a list of all known wrappers that have been created using nimterop. They will provide real world examples of how to wrap libraries. Please do add your project once you are done so that others can benefit from your work.
#### Build API
### Build API
Creating a wrapper has two parts, the first is to setup the C library. This includes downloading it or finding it if already installed, and building it if applicable. The `getHeader()` high-level API provides all of this functionality as a convenience. Following is an example of using the high-level `getHeader()` API to perform all building and linking automatically:
@ -103,7 +103,7 @@ The `-d:xxxYYY` Nim define flags have already been described above and can be sp
If more fine-tuned control is desired over the build process, it is possible to manually control all steps that `getHeader()` performs by directly using the API provided by [build](https://nimterop.github.io/nimterop/build.html). Note also that there is no requirement to use these APIs to setup the library. Any other established mechanisms can be used to do so any limitations imposed by Nimterop are unintentional and feedback is most welcome.
#### Wrapper API
### Wrapper API
Once the C library is setup, the next step is to create wrappers that inform Nim of all the types and functions that are available. Following is a simple example covering the API:
@ -152,7 +152,7 @@ __Compiling source__
The job of building and compiling the underlying C library is best left to the build mechanism selected by the library author so using `getHeader()` is recommended. For simpler projects with a few `.c` files though, `cCompile()` should be more than enough. It is not recommended for larger projects which heavily rely on functionality offered by build tools. Recreating reliable logic in Nim can be tedious and one can expect minimal support from that author if their tested build mechanism is not used.
#### Command line API
### Command line API
The `toast` binary can also be used directly on the CLI, similar to `c2nim`. The `cPlugin()` interface
@ -190,7 +190,7 @@ Options:
-O=, --symOverride= strings {} skip generating specified symbols
```
### Why nimterop
## Why nimterop
Nim has one of the best FFI you can find - importing C/C++ is supported out of the box. All you need to provide is type and proc definitions for Nim to interop with C/C++ binaries. Generation of these wrappers is easy for simple libraries but can quickly get out of hand. [c2nim](https://github.com/nim-lang/c2nim) greatly helps here by parsing and converting C/C++ into Nim but is limited due to the complex and constantly evolving C/C++ grammar. [nimgen](https://github.com/genotrance/nimgen) mainly focused on automating the wrapping process with `c2nim` and filled some holes but is again limited to `c2nim` capabilities.
@ -212,10 +212,10 @@ The con of this approach of delegating to the preprocessor is that the Nim wrapp
This is part of the reason why Nimterop provides a wrapper API so that the generation of wrappers is Nim code that can be rendered as part of the build process on the target platform. It helps to think of Nimterop as a build time tool like `cmake` that renders artifacts on the target rather than a tool whose generated artifacts should be checked into source control. Regardless, both the wrapper API and the `toast` command line still allow saving the wrapper output to a file to be stored in source control since it might work well enough for many projects.
__Credits__
## Credits
Nimterop depends on [tree-sitter](http://tree-sitter.github.io/tree-sitter/) and all licensing terms of [tree-sitter](https://github.com/tree-sitter/tree-sitter/blob/master/LICENSE) apply to the usage of this package. The tree-sitter functionality is pulled and wrapped using nimterop itself.
__Feedback__
## Feedback
Nimterop is a work in progress and any feedback or suggestions are welcome. It is hosted on [GitHub](https://github.com/nimterop/nimterop) with an MIT license so issues, forks and PRs are most appreciated.

View file

@ -4,11 +4,32 @@ import os except findExe, sleep
import regex
proc fixCmd(cmd: string): string =
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}"
result = "cmd /c " & filteredCmd
elif defined(posix):
result = cmd
else:
doAssert false
proc sanitizePath*(path: string, noQuote = false, sep = $DirSep): string =
result = path.multiReplace([("\\\\", sep), ("\\", sep), ("/", sep)])
if not noQuote:
result = result.quoteShell
# Nim cfg file related functionality
include "."/nimconf
proc sleep*(milsecs: int) =
## Sleep at compile time
let
@ -20,14 +41,9 @@ proc sleep*(milsecs: int) =
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"
# Get location to cache all nimterop artifacts
result = getNimcacheDir() / "nimterop"
proc getCurrentNimCompiler*(): string =
result = getCurrentCompilerExe()
@ -46,24 +62,8 @@ proc execAction*(cmd: string, retry = 0, die = true, cache = false,
## `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
let
ccmd = fixCmd(cmd)
when nimvm:
# Cache results for speedup if cache = true

View file

@ -286,6 +286,7 @@ proc cPluginHelper(body: string, imports = "import macros, nimterop/plugin\n\n")
if not fileExists(path) or gStateCT.nocache or compileOption("forceBuild"):
mkDir(path.parentDir())
writeFile(path, data)
writeNimConfig(path & ".cfg")
doAssert fileExists(path), "Unable to write plugin file: " & path

View file

@ -2,8 +2,11 @@ import macros, strformat
from os import parentDir, getCurrentCompilerExe, DirSep
when defined(nimdoc) or (NimMajor, NimMinor) >= (1, 3):
from os import paramCount, paramStr
when defined(nimdoc):
from os import getCurrentDir, paramCount, paramStr
from os import getCurrentDir
proc getNimRootDir(): string =
#[

199
nimterop/nimconf.nim Normal file
View file

@ -0,0 +1,199 @@
import json, macros, os, osproc, sets, strformat, strutils
when nimvm:
when (NimMajor, NimMinor, NimPatch) >= (1, 2, 0):
import std/compilesettings
else:
discard
# Config detected with std/compilesettings or `nim dump`
type
Config* = ref object
NimMajor*: int
NimMinor*: int
NimPatch*: int
paths*: OrderedSet[string]
nimblePaths*: OrderedSet[string]
nimcacheDir*: string
proc getJson(projectDir: string): JsonNode =
# Get `nim dump` json value for `projectDir`
var
cmd = "nim --hints:off --dump.format:json dump dummy"
dump = ""
ret = 0
if projectDir.len != 0:
# Run `nim dump` in `projectDir` if specified
cmd = &"cd {projectDir.sanitizePath} && " & cmd
cmd = fixCmd(cmd)
when nimvm:
(dump, ret) = gorgeEx(cmd)
else:
(dump, ret) = execCmdEx(cmd)
try:
result = parseJson(dump)
except JsonParsingError as e:
echo "# Failed to parse `nim dump` output: " & e.msg
proc getOsCacheDir(): string =
# OS default cache directory
when defined(posix):
result = getEnv("XDG_CACHE_HOME", getHomeDir() / ".cache") / "nim"
else:
result = getHomeDir() / "nimcache"
proc getProjectDir*(): string =
## Get project directory for this compilation - returns `""` at runtime
when nimvm:
when (NimMajor, NimMinor, NimPatch) >= (1, 2, 0):
# If nim v1.2.0+, get from `std/compilesettings`
result = querySetting(projectFull).parentDir()
else:
# Get from `macros`
result = getProjectPath()
else:
discard
proc getNimcacheDir*(projectDir = ""): string =
## Get nimcache directory for current compilation or specified `projectDir`
when nimvm:
when (NimMajor, NimMinor, NimPatch) >= (1, 2, 0):
# Get value at compile time from `std/compilesettings`
result = querySetting(SingleValueSetting.nimcacheDir)
else:
discard
# Not Nim v1.2.0+ or runtime
if result.len == 0:
let
# Get project directory for < v1.2.0 at compile time
projectDir = if projectDir.len != 0: projectDir else: getProjectDir()
# Use `nim dump` to figure out nimcache for `projectDir`
let
dumpJson = getJson(projectDir)
if dumpJson != nil and dumpJson.hasKey("nimcache"):
result = dumpJson["nimcache"].getStr()
let
(head, tail) = result.splitPath()
if "dummy" in tail:
# Remove `dummy_d` subdir when default nimcache
result = head
# Set to OS defaults if not detectable
if result.len == 0:
result = getOsCacheDir()
proc jsonToSeq(node: JsonNode, key: string): seq[string] =
# Convert JsonArray to seq[string] for specified `key`
if node.hasKey(key):
for elem in node[key].getElems():
result.add elem.getStr()
proc getNimConfig*(projectDir = ""): Config =
# Get `paths` - list of paths to be forwarded to Nim
result = new(Config)
var
libPath, version: string
lazyPaths, searchPaths: seq[string]
when nimvm:
result.NimMajor = NimMajor
result.NimMinor = NimMinor
result.NimPatch = NimPatch
when (NimMajor, NimMinor, NimPatch) >= (1, 2, 0):
# Get value at compile time from `std/compilesettings`
libPath = getCurrentCompilerExe().parentDir().parentDir() / "lib"
lazyPaths = querySettingSeq(MultipleValueSetting.lazyPaths)
searchPaths = querySettingSeq(MultipleValueSetting.searchPaths)
result.nimcacheDir = querySetting(SingleValueSetting.nimcacheDir)
else:
discard
let
# Get project directory for < v1.2.0 at compile time
projectDir = if projectDir.len != 0: projectDir else: getProjectDir()
# Not Nim v1.2.0+ or runtime
if libPath.len == 0:
let
dumpJson = getJson(projectDir)
if dumpJson != nil:
if dumpJson.hasKey("version"):
version = dumpJson["version"].getStr()
lazyPaths = jsonToSeq(dumpJson, "lazyPaths")
searchPaths = jsonToSeq(dumpJson, "lib_paths")
if dumpJson.hasKey("libpath"):
libPath = dumpJson["libpath"].getStr()
elif searchPaths.len != 0:
# Usually `libPath` is last entry in `searchPaths`
libPath = searchPaths[^1]
# Parse version
if version.len != 0:
let
splversion = version.split({'.'}, maxsplit = 3)
result.NimMajor = splversion[0].parseInt()
result.NimMinor = splversion[1].parseInt()
result.NimPatch = splversion[2].parseInt()
# Find non standard lib paths added to `searchPath`
for path in searchPaths:
if libPath notin path:
result.paths.incl path
# Find `nimblePaths` in `lazyPaths`
for path in lazyPaths:
let
(_, tail) = path.strip(leading = false, chars = {'/', '\\'}).splitPath()
if tail == "pkgs":
# Nimble path probably
result.nimblePaths.incl path
# Find `paths` in `lazyPaths` that aren't within `nimblePaths`
# Have to do this separately since `nimblePaths` could be after
# packages in `lazyPaths`
for path in lazyPaths:
var skip = false
for npath in result.nimblePaths:
if npath in path:
skip = true
break
if not skip:
result.paths.incl path
result.nimcacheDir = getNimcacheDir(projectDir)
proc writeNimConfig*(cfg: Config, cfgFile: string) =
# Write Nim configuration to file
var
cfgOut = &"--nimcache:\"{cfg.nimcacheDir}\"\n"
if (cfg.NimMajor, cfg.NimMinor, cfg.NimPatch) >= (1, 2, 0):
# --clearNimbleCache if Nim v1.2.0+
cfgOut &= "--clearNimblePath\n"
# Add `nimblePaths` if detected - v1.2.0+
for path in cfg.nimblePaths:
cfgOut &= &"--nimblePath:\"{path}\"\n"
# Add `paths` in all cases if any detected
for path in cfg.paths:
cfgOut &= &"--path:\"{path}\"\n"
when defined(windows):
cfgOut = cfgOut.replace("\\", "/")
writeFile(cfgFile, cfgOut)
proc writeNimConfig*(cfgFile: string, projectDir = "") =
let
cfg = getNimConfig(projectDir)
writeNimConfig(cfg, cfgFile)

View file

@ -11,6 +11,7 @@ proc testCall(cmd, output: string, exitCode: int, delete = true) =
if not delete:
ccmd = ccmd.replace(" -f ", " ")
echo ccmd
var
(outp, exitC) = gorgeEx(ccmd)
echo outp