mirror of
https://github.com/nim-lang/Nim.git
synced 2026-02-12 14:23:45 +00:00
396 lines
12 KiB
Nim
396 lines
12 KiB
Nim
#
|
|
#
|
|
# The Nim Compiler
|
|
# (c) Copyright 2025 Andreas Rumpf
|
|
#
|
|
# See the file "copying.txt", included in this
|
|
# distribution, for details about the copyright.
|
|
#
|
|
|
|
## Generate a .build.nif file for nifmake from a Nim project.
|
|
## This enables incremental and parallel compilation using the `m` switch.
|
|
|
|
import std / [os, tables, sets, times, osproc, strutils]
|
|
import options, msgs, lineinfos, pathutils
|
|
|
|
import "../dist/nimony/src/lib" / [nifstreams, nifcursors, bitabs, nifreader, nifbuilder]
|
|
import "../dist/nimony/src/gear2" / modnames
|
|
|
|
type
|
|
FilePair = object
|
|
nimFile: string
|
|
modname: string
|
|
|
|
Node = ref object
|
|
files: seq[FilePair] # main file + includes
|
|
deps: seq[int] # indices into DepContext.nodes
|
|
id: int
|
|
|
|
DepContext = object
|
|
config: ConfigRef
|
|
nifler: string
|
|
nodes: seq[Node]
|
|
processedModules: Table[string, int] # modname -> node index
|
|
includeStack: seq[string]
|
|
systemNodeId: int # ID of the system.nim node
|
|
|
|
proc toPair(c: DepContext; f: string): FilePair =
|
|
FilePair(nimFile: f, modname: moduleSuffix(f, cast[seq[string]](c.config.searchPaths)))
|
|
|
|
proc depsFile(c: DepContext; f: FilePair): string =
|
|
getNimcacheDir(c.config).string / f.modname & ".deps.nif"
|
|
|
|
proc parsedFile(c: DepContext; f: FilePair): string =
|
|
getNimcacheDir(c.config).string / f.modname & ".p.nif"
|
|
|
|
proc semmedFile(c: DepContext; f: FilePair): string =
|
|
getNimcacheDir(c.config).string / f.modname & ".nif"
|
|
|
|
proc findNifler(): string =
|
|
# Look for nifler in common locations
|
|
let nimDir = getAppDir()
|
|
result = nimDir / "nifler"
|
|
if not fileExists(result):
|
|
result = findExe("nifler")
|
|
|
|
proc findNifmake(): string =
|
|
# Look for nifmake in common locations
|
|
# Try relative to nim executable
|
|
let nimDir = getAppDir()
|
|
result = nimDir / "nifmake"
|
|
if not fileExists(result):
|
|
result = findExe("nifmake")
|
|
|
|
proc runNifler(c: DepContext; nimFile: string): bool =
|
|
## Run nifler deps on a file if needed. Returns true on success.
|
|
let pair = c.toPair(nimFile)
|
|
let depsPath = c.depsFile(pair)
|
|
|
|
# Check if deps file is up-to-date
|
|
if fileExists(depsPath) and fileExists(nimFile):
|
|
if getLastModificationTime(depsPath) > getLastModificationTime(nimFile):
|
|
return true # Already up-to-date
|
|
|
|
# Create output directory if needed
|
|
createDir(parentDir(depsPath))
|
|
|
|
# Run nifler deps
|
|
let cmd = quoteShell(c.nifler) & " deps " & quoteShell(nimFile) & " " & quoteShell(depsPath)
|
|
let exitCode = execShellCmd(cmd)
|
|
result = exitCode == 0
|
|
|
|
proc resolveFile(c: DepContext; origin, toResolve: string): string =
|
|
## Resolve an import path relative to origin file
|
|
# Handle std/ prefix
|
|
var path = toResolve
|
|
if path.startsWith("std/"):
|
|
path = path.substr(4)
|
|
|
|
# Try relative to origin first
|
|
let originDir = parentDir(origin)
|
|
result = originDir / path.addFileExt("nim")
|
|
if fileExists(result):
|
|
return result
|
|
|
|
# Try search paths
|
|
for searchPath in c.config.searchPaths:
|
|
result = searchPath.string / path.addFileExt("nim")
|
|
if fileExists(result):
|
|
return result
|
|
|
|
result = ""
|
|
|
|
proc traverseDeps(c: var DepContext; pair: FilePair; current: Node)
|
|
|
|
proc processInclude(c: var DepContext; includePath: string; current: Node) =
|
|
let resolved = resolveFile(c, current.files[current.files.len - 1].nimFile, includePath)
|
|
if resolved.len == 0 or not fileExists(resolved):
|
|
return
|
|
|
|
# Check for recursive includes
|
|
for s in c.includeStack:
|
|
if s == resolved:
|
|
return # Skip recursive include
|
|
|
|
c.includeStack.add resolved
|
|
current.files.add c.toPair(resolved)
|
|
traverseDeps(c, c.toPair(resolved), current)
|
|
discard c.includeStack.pop()
|
|
|
|
proc processImport(c: var DepContext; importPath: string; current: Node) =
|
|
let resolved = resolveFile(c, current.files[0].nimFile, importPath)
|
|
if resolved.len == 0 or not fileExists(resolved):
|
|
return
|
|
|
|
let pair = c.toPair(resolved)
|
|
let existingIdx = c.processedModules.getOrDefault(pair.modname, -1)
|
|
|
|
if existingIdx == -1:
|
|
# New module - create node and process it
|
|
let newNode = Node(files: @[pair], id: c.nodes.len)
|
|
current.deps.add newNode.id
|
|
# Every module depends on system.nim
|
|
if c.systemNodeId >= 0:
|
|
newNode.deps.add c.systemNodeId
|
|
c.processedModules[pair.modname] = newNode.id
|
|
c.nodes.add newNode
|
|
traverseDeps(c, pair, newNode)
|
|
else:
|
|
# Already processed - just add dependency
|
|
if existingIdx notin current.deps:
|
|
current.deps.add existingIdx
|
|
|
|
proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) =
|
|
## Read a .deps.nif file and process imports/includes
|
|
let depsPath = c.depsFile(pair)
|
|
if not fileExists(depsPath):
|
|
return
|
|
|
|
var s = nifstreams.open(depsPath)
|
|
defer: nifstreams.close(s)
|
|
discard processDirectives(s.r)
|
|
|
|
var t = next(s)
|
|
if t.kind != ParLe:
|
|
return
|
|
|
|
# Skip to content (past stmts tag)
|
|
t = next(s)
|
|
|
|
while t.kind != EofToken:
|
|
if t.kind == ParLe:
|
|
let tag = pool.tags[t.tagId]
|
|
case tag
|
|
of "import", "fromimport":
|
|
# Read import path
|
|
t = next(s)
|
|
# Check for "when" marker (conditional import)
|
|
if t.kind == Ident and pool.strings[t.litId] == "when":
|
|
t = next(s) # skip it, still process the import
|
|
# Handle path expression (could be ident, string, or infix like std/foo)
|
|
var importPath = ""
|
|
if t.kind == Ident:
|
|
importPath = pool.strings[t.litId]
|
|
elif t.kind == StringLit:
|
|
importPath = pool.strings[t.litId]
|
|
elif t.kind == ParLe and pool.tags[t.tagId] == "infix":
|
|
# Handle std / foo style imports
|
|
t = next(s) # skip infix tag
|
|
if t.kind == Ident: # operator (/)
|
|
t = next(s)
|
|
if t.kind == Ident: # first part (std)
|
|
importPath = pool.strings[t.litId]
|
|
t = next(s)
|
|
if t.kind == Ident: # second part (foo)
|
|
importPath = importPath & "/" & pool.strings[t.litId]
|
|
if importPath.len > 0:
|
|
processImport(c, importPath, current)
|
|
# Skip to end of import node
|
|
var depth = 1
|
|
while depth > 0:
|
|
t = next(s)
|
|
if t.kind == ParLe: inc depth
|
|
elif t.kind == ParRi: dec depth
|
|
of "include":
|
|
# Read include path
|
|
t = next(s)
|
|
if t.kind == Ident and pool.strings[t.litId] == "when":
|
|
t = next(s) # skip conditional marker
|
|
var includePath = ""
|
|
if t.kind == Ident:
|
|
includePath = pool.strings[t.litId]
|
|
elif t.kind == StringLit:
|
|
includePath = pool.strings[t.litId]
|
|
if includePath.len > 0:
|
|
processInclude(c, includePath, current)
|
|
# Skip to end
|
|
var depth = 1
|
|
while depth > 0:
|
|
t = next(s)
|
|
if t.kind == ParLe: inc depth
|
|
elif t.kind == ParRi: dec depth
|
|
else:
|
|
# Skip unknown node
|
|
var depth = 1
|
|
while depth > 0:
|
|
t = next(s)
|
|
if t.kind == ParLe: inc depth
|
|
elif t.kind == ParRi: dec depth
|
|
t = next(s)
|
|
|
|
proc traverseDeps(c: var DepContext; pair: FilePair; current: Node) =
|
|
## Process a module: run nifler and read deps
|
|
if not runNifler(c, pair.nimFile):
|
|
rawMessage(c.config, errGenerated, "nifler failed for: " & pair.nimFile)
|
|
return
|
|
readDepsFile(c, pair, current)
|
|
|
|
proc generateBuildFile(c: DepContext): string =
|
|
## Generate the .build.nif file for nifmake
|
|
let nimcache = getNimcacheDir(c.config).string
|
|
createDir(nimcache)
|
|
result = nimcache / c.nodes[0].files[0].modname & ".build.nif"
|
|
|
|
var b = nifbuilder.open(result)
|
|
defer: b.close()
|
|
|
|
b.addHeader("nim ic", "nifmake")
|
|
b.addTree "stmts"
|
|
|
|
# Define nifler command
|
|
b.addTree "cmd"
|
|
b.addSymbolDef "nifler"
|
|
b.addStrLit c.nifler
|
|
b.addStrLit "parse"
|
|
b.addStrLit "--deps"
|
|
b.addTree "input"
|
|
b.endTree()
|
|
b.addTree "output"
|
|
b.endTree()
|
|
b.endTree()
|
|
|
|
# Define nim m command
|
|
b.addTree "cmd"
|
|
b.addSymbolDef "nim_m"
|
|
b.addStrLit getAppFilename()
|
|
b.addStrLit "m"
|
|
b.addStrLit "--nimcache:" & nimcache
|
|
# Add search paths
|
|
for p in c.config.searchPaths:
|
|
b.addStrLit "--path:" & p.string
|
|
b.addTree "args"
|
|
b.endTree()
|
|
b.withTree "input":
|
|
b.addIntLit 0 # main parsed file
|
|
b.endTree()
|
|
|
|
# Define nim nifc command
|
|
b.addTree "cmd"
|
|
b.addSymbolDef "nim_nifc"
|
|
b.addStrLit getAppFilename()
|
|
b.addStrLit "nifc"
|
|
b.addStrLit "--nimcache:" & nimcache
|
|
# Add search paths
|
|
for p in c.config.searchPaths:
|
|
b.addStrLit "--path:" & p.string
|
|
b.addTree "input"
|
|
b.addIntLit 0
|
|
b.endTree()
|
|
b.endTree()
|
|
|
|
# Build rules for parsing (nifler)
|
|
var seenFiles = initHashSet[string]()
|
|
for node in c.nodes:
|
|
for pair in node.files:
|
|
let parsed = c.parsedFile(pair)
|
|
if not seenFiles.containsOrIncl(parsed):
|
|
b.addTree "do"
|
|
b.addIdent "nifler"
|
|
b.addTree "input"
|
|
b.addStrLit pair.nimFile
|
|
b.endTree()
|
|
b.addTree "output"
|
|
b.addStrLit parsed
|
|
b.endTree()
|
|
b.addTree "output"
|
|
b.addStrLit c.depsFile(pair)
|
|
b.endTree()
|
|
b.endTree()
|
|
|
|
# Build rules for semantic checking (nim m)
|
|
for i in countdown(c.nodes.len - 1, 0):
|
|
let node = c.nodes[i]
|
|
let pair = node.files[0]
|
|
b.addTree "do"
|
|
b.addIdent "nim_m"
|
|
# Input: all parsed files for this module
|
|
b.withTree "input":
|
|
b.addStrLit node.files[0].nimFile
|
|
for f in node.files:
|
|
b.addTree "input"
|
|
b.addStrLit c.parsedFile(f)
|
|
b.endTree()
|
|
# Also depend on semmed files of dependencies
|
|
for depIdx in node.deps:
|
|
b.addTree "input"
|
|
b.addStrLit c.semmedFile(c.nodes[depIdx].files[0])
|
|
b.endTree()
|
|
# Output: semmed file
|
|
b.addTree "output"
|
|
b.addStrLit c.semmedFile(pair)
|
|
b.endTree()
|
|
b.endTree()
|
|
|
|
# Final compilation step: generate executable from main module
|
|
let mainNif = c.nodes[0].files[0].nimFile
|
|
let exeFile = changeFileExt(c.nodes[0].files[0].nimFile, ExeExt)
|
|
b.addTree "do"
|
|
b.addIdent "nim_nifc"
|
|
# Input: .nim file (expanded as argument) and .nif file (dependency)
|
|
b.addTree "input"
|
|
b.addStrLit mainNif
|
|
b.endTree()
|
|
b.addTree "output"
|
|
b.addStrLit exeFile
|
|
b.endTree()
|
|
b.endTree()
|
|
|
|
b.endTree() # stmts
|
|
|
|
proc commandIc*(conf: ConfigRef) =
|
|
## Main entry point for `nim ic`
|
|
when not defined(nimKochBootstrap):
|
|
let nifler = findNifler()
|
|
if nifler.len == 0:
|
|
rawMessage(conf, errGenerated, "nifler tool not found. Install nimony or add nifler to PATH.")
|
|
return
|
|
|
|
let projectFile = conf.projectFull.string
|
|
if not fileExists(projectFile):
|
|
rawMessage(conf, errGenerated, "project file not found: " & projectFile)
|
|
return
|
|
|
|
# Create nimcache directory
|
|
createDir(getNimcacheDir(conf).string)
|
|
|
|
var c = DepContext(
|
|
config: conf,
|
|
nifler: nifler,
|
|
nodes: @[],
|
|
processedModules: initTable[string, int](),
|
|
includeStack: @[],
|
|
systemNodeId: -1
|
|
)
|
|
|
|
# Create root node for main project file
|
|
let rootPair = c.toPair(projectFile)
|
|
let rootNode = Node(files: @[rootPair], id: 0)
|
|
c.nodes.add rootNode
|
|
c.processedModules[rootPair.modname] = 0
|
|
|
|
# model the system.nim dependency:
|
|
let sysNode = Node(files: @[toPair(c, (conf.libpath / RelativeFile"system.nim").string)], id: 1)
|
|
c.nodes.add sysNode
|
|
c.systemNodeId = sysNode.id
|
|
rootNode.deps.add sysNode.id
|
|
|
|
# Process dependencies
|
|
traverseDeps(c, rootPair, rootNode)
|
|
|
|
# Generate build file
|
|
let buildFile = generateBuildFile(c)
|
|
rawMessage(conf, hintSuccess, "generated: " & buildFile)
|
|
|
|
# Automatically run nifmake
|
|
let nifmake = findNifmake()
|
|
if nifmake.len == 0:
|
|
rawMessage(conf, hintSuccess, "run: nifmake run " & buildFile)
|
|
else:
|
|
let cmd = quoteShell(nifmake) & " run " & quoteShell(buildFile)
|
|
rawMessage(conf, hintExecuting, cmd)
|
|
let exitCode = execShellCmd(cmd)
|
|
if exitCode != 0:
|
|
rawMessage(conf, errGenerated, "nifmake failed with exit code: " & $exitCode)
|
|
else:
|
|
rawMessage(conf, errGenerated, "nim ic not available in bootstrap build")
|