Skip to content

Commit 7ff9dc7

Browse files
committed
nim deps command
1 parent 54d5535 commit 7ff9dc7

File tree

4 files changed

+350
-0
lines changed

4 files changed

+350
-0
lines changed

compiler/commands.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ proc parseCommand*(command: string): Command =
499499
of "nop", "help": cmdNop
500500
of "jsonscript": cmdJsonscript
501501
of "nifc": cmdNifC # generate C from NIF files
502+
of "deps": cmdDeps # generate .build.nif for nifmake
502503
else: cmdUnknown
503504

504505
proc setCmd*(conf: ConfigRef, cmd: Command) =

compiler/deps.nim

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
#
2+
#
3+
# The Nim Compiler
4+
# (c) Copyright 2025 Andreas Rumpf
5+
#
6+
# See the file "copying.txt", included in this
7+
# distribution, for details about the copyright.
8+
#
9+
10+
## Generate a .build.nif file for nifmake from a Nim project.
11+
## This enables incremental and parallel compilation using the `m` switch.
12+
13+
import std / [os, tables, sets, times, osproc, strutils]
14+
import options, msgs, pathutils, lineinfos
15+
16+
import "../dist/nimony/src/lib" / [nifstreams, nifcursors, bitabs, nifreader, nifbuilder]
17+
import "../dist/nimony/src/gear2" / modnames
18+
19+
type
20+
FilePair = object
21+
nimFile: string
22+
modname: string
23+
24+
Node = ref object
25+
files: seq[FilePair] # main file + includes
26+
deps: seq[int] # indices into DepContext.nodes
27+
id: int
28+
29+
DepContext = object
30+
config: ConfigRef
31+
nifler: string
32+
nodes: seq[Node]
33+
processedModules: Table[string, int] # modname -> node index
34+
includeStack: seq[string]
35+
36+
proc toPair(c: DepContext; f: string): FilePair =
37+
FilePair(nimFile: f, modname: moduleSuffix(f, cast[seq[string]](c.config.searchPaths)))
38+
39+
proc depsFile(c: DepContext; f: FilePair): string =
40+
getNimcacheDir(c.config).string / f.modname & ".deps.nif"
41+
42+
proc parsedFile(c: DepContext; f: FilePair): string =
43+
getNimcacheDir(c.config).string / f.modname & ".p.nif"
44+
45+
proc semmedFile(c: DepContext; f: FilePair): string =
46+
getNimcacheDir(c.config).string / f.modname & ".nif"
47+
48+
proc findNifler(): string =
49+
# Look for nifler in common locations
50+
result = findExe("nifler")
51+
if result.len == 0:
52+
# Try relative to nim executable
53+
let nimDir = getAppDir()
54+
result = nimDir / "nifler"
55+
if not fileExists(result):
56+
result = nimDir / ".." / "nimony" / "bin" / "nifler"
57+
if not fileExists(result):
58+
result = ""
59+
60+
proc runNifler(c: DepContext; nimFile: string): bool =
61+
## Run nifler deps on a file if needed. Returns true on success.
62+
let pair = c.toPair(nimFile)
63+
let depsPath = c.depsFile(pair)
64+
65+
# Check if deps file is up-to-date
66+
if fileExists(depsPath) and fileExists(nimFile):
67+
if getLastModificationTime(depsPath) > getLastModificationTime(nimFile):
68+
return true # Already up-to-date
69+
70+
# Create output directory if needed
71+
createDir(parentDir(depsPath))
72+
73+
# Run nifler deps
74+
let cmd = quoteShell(c.nifler) & " deps " & quoteShell(nimFile) & " " & quoteShell(depsPath)
75+
let exitCode = execShellCmd(cmd)
76+
result = exitCode == 0
77+
78+
proc resolveFile(c: DepContext; origin, toResolve: string): string =
79+
## Resolve an import path relative to origin file
80+
# Handle std/ prefix
81+
var path = toResolve
82+
if path.startsWith("std/"):
83+
path = path.substr(4)
84+
85+
# Try relative to origin first
86+
let originDir = parentDir(origin)
87+
result = originDir / path.addFileExt("nim")
88+
if fileExists(result):
89+
return result
90+
91+
# Try search paths
92+
for searchPath in c.config.searchPaths:
93+
result = searchPath.string / path.addFileExt("nim")
94+
if fileExists(result):
95+
return result
96+
97+
result = ""
98+
99+
proc traverseDeps(c: var DepContext; pair: FilePair; current: Node)
100+
101+
proc processInclude(c: var DepContext; includePath: string; current: Node) =
102+
let resolved = resolveFile(c, current.files[current.files.len - 1].nimFile, includePath)
103+
if resolved.len == 0 or not fileExists(resolved):
104+
return
105+
106+
# Check for recursive includes
107+
for s in c.includeStack:
108+
if s == resolved:
109+
return # Skip recursive include
110+
111+
c.includeStack.add resolved
112+
current.files.add c.toPair(resolved)
113+
traverseDeps(c, c.toPair(resolved), current)
114+
discard c.includeStack.pop()
115+
116+
proc processImport(c: var DepContext; importPath: string; current: Node) =
117+
let resolved = resolveFile(c, current.files[0].nimFile, importPath)
118+
if resolved.len == 0 or not fileExists(resolved):
119+
return
120+
121+
let pair = c.toPair(resolved)
122+
let existingIdx = c.processedModules.getOrDefault(pair.modname, -1)
123+
124+
if existingIdx == -1:
125+
# New module - create node and process it
126+
let newNode = Node(files: @[pair], id: c.nodes.len)
127+
current.deps.add newNode.id
128+
c.processedModules[pair.modname] = newNode.id
129+
c.nodes.add newNode
130+
traverseDeps(c, pair, newNode)
131+
else:
132+
# Already processed - just add dependency
133+
if existingIdx notin current.deps:
134+
current.deps.add existingIdx
135+
136+
proc readDepsFile(c: var DepContext; pair: FilePair; current: Node) =
137+
## Read a .deps.nif file and process imports/includes
138+
let depsPath = c.depsFile(pair)
139+
if not fileExists(depsPath):
140+
return
141+
142+
var s = nifstreams.open(depsPath)
143+
defer: nifstreams.close(s)
144+
discard processDirectives(s.r)
145+
146+
var t = next(s)
147+
if t.kind != ParLe:
148+
return
149+
150+
# Skip to content (past stmts tag)
151+
t = next(s)
152+
153+
while t.kind != EofToken:
154+
if t.kind == ParLe:
155+
let tag = pool.tags[t.tagId]
156+
case tag
157+
of "import", "fromimport":
158+
# Read import path
159+
t = next(s)
160+
# Check for "when" marker (conditional import)
161+
if t.kind == Ident and pool.strings[t.litId] == "when":
162+
t = next(s) # skip it, still process the import
163+
# Handle path expression (could be ident, string, or infix like std/foo)
164+
var importPath = ""
165+
if t.kind == Ident:
166+
importPath = pool.strings[t.litId]
167+
elif t.kind == StringLit:
168+
importPath = pool.strings[t.litId]
169+
elif t.kind == ParLe and pool.tags[t.tagId] == "infix":
170+
# Handle std / foo style imports
171+
t = next(s) # skip infix tag
172+
if t.kind == Ident: # operator (/)
173+
t = next(s)
174+
if t.kind == Ident: # first part (std)
175+
importPath = pool.strings[t.litId]
176+
t = next(s)
177+
if t.kind == Ident: # second part (foo)
178+
importPath = importPath & "/" & pool.strings[t.litId]
179+
if importPath.len > 0:
180+
processImport(c, importPath, current)
181+
# Skip to end of import node
182+
var depth = 1
183+
while depth > 0:
184+
t = next(s)
185+
if t.kind == ParLe: inc depth
186+
elif t.kind == ParRi: dec depth
187+
of "include":
188+
# Read include path
189+
t = next(s)
190+
if t.kind == Ident and pool.strings[t.litId] == "when":
191+
t = next(s) # skip conditional marker
192+
var includePath = ""
193+
if t.kind == Ident:
194+
includePath = pool.strings[t.litId]
195+
elif t.kind == StringLit:
196+
includePath = pool.strings[t.litId]
197+
if includePath.len > 0:
198+
processInclude(c, includePath, current)
199+
# Skip to end
200+
var depth = 1
201+
while depth > 0:
202+
t = next(s)
203+
if t.kind == ParLe: inc depth
204+
elif t.kind == ParRi: dec depth
205+
else:
206+
# Skip unknown node
207+
var depth = 1
208+
while depth > 0:
209+
t = next(s)
210+
if t.kind == ParLe: inc depth
211+
elif t.kind == ParRi: dec depth
212+
t = next(s)
213+
214+
proc traverseDeps(c: var DepContext; pair: FilePair; current: Node) =
215+
## Process a module: run nifler and read deps
216+
if not runNifler(c, pair.nimFile):
217+
rawMessage(c.config, errGenerated, "nifler failed for: " & pair.nimFile)
218+
return
219+
readDepsFile(c, pair, current)
220+
221+
proc generateBuildFile(c: DepContext): string =
222+
## Generate the .build.nif file for nifmake
223+
result = getNimcacheDir(c.config).string / c.nodes[0].files[0].modname & ".build.nif"
224+
225+
var b = nifbuilder.open(result)
226+
defer: b.close()
227+
228+
b.addHeader("nim deps", "nifmake")
229+
b.addTree "stmts"
230+
231+
# Define nifler command
232+
b.addTree "cmd"
233+
b.addSymbolDef "nifler"
234+
b.addStrLit c.nifler
235+
b.addStrLit "parse"
236+
b.addStrLit "--deps"
237+
b.addTree "input"
238+
b.endTree()
239+
b.addTree "output"
240+
b.endTree()
241+
b.endTree()
242+
243+
# Define nim m command
244+
b.addTree "cmd"
245+
b.addSymbolDef "nim_m"
246+
b.addStrLit getAppFilename()
247+
b.addStrLit "m"
248+
# Add search paths
249+
for p in c.config.searchPaths:
250+
b.addStrLit "--path:" & p.string
251+
b.addTree "input"
252+
b.addIntLit 0
253+
b.endTree()
254+
b.endTree()
255+
256+
# Build rules for parsing (nifler)
257+
var seenFiles = initHashSet[string]()
258+
for node in c.nodes:
259+
for pair in node.files:
260+
let parsed = c.parsedFile(pair)
261+
if not seenFiles.containsOrIncl(parsed):
262+
b.addTree "do"
263+
b.addIdent "nifler"
264+
b.addTree "input"
265+
b.addStrLit pair.nimFile
266+
b.endTree()
267+
b.addTree "output"
268+
b.addStrLit parsed
269+
b.endTree()
270+
b.addTree "output"
271+
b.addStrLit c.depsFile(pair)
272+
b.endTree()
273+
b.endTree()
274+
275+
# Build rules for semantic checking (nim m)
276+
for i in countdown(c.nodes.len - 1, 0):
277+
let node = c.nodes[i]
278+
let pair = node.files[0]
279+
b.addTree "do"
280+
b.addIdent "nim_m"
281+
# Input: all parsed files for this module
282+
for f in node.files:
283+
b.addTree "input"
284+
b.addStrLit c.parsedFile(f)
285+
b.endTree()
286+
# Also depend on semmed files of dependencies
287+
for depIdx in node.deps:
288+
b.addTree "input"
289+
b.addStrLit c.semmedFile(c.nodes[depIdx].files[0])
290+
b.endTree()
291+
# Output: semmed file
292+
b.addTree "output"
293+
b.addStrLit c.semmedFile(pair)
294+
b.endTree()
295+
b.addTree "args"
296+
b.addStrLit pair.nimFile
297+
b.endTree()
298+
b.endTree()
299+
300+
b.endTree() # stmts
301+
302+
proc commandDeps*(conf: ConfigRef) =
303+
## Main entry point for `nim deps`
304+
when not defined(nimKochBootstrap):
305+
let nifler = findNifler()
306+
if nifler.len == 0:
307+
rawMessage(conf, errGenerated, "nifler tool not found. Install nimony or add nifler to PATH.")
308+
return
309+
310+
let projectFile = conf.projectFull.string
311+
if not fileExists(projectFile):
312+
rawMessage(conf, errGenerated, "project file not found: " & projectFile)
313+
return
314+
315+
# Create nimcache directory
316+
createDir(getNimcacheDir(conf).string)
317+
318+
var c = DepContext(
319+
config: conf,
320+
nifler: nifler,
321+
nodes: @[],
322+
processedModules: initTable[string, int](),
323+
includeStack: @[]
324+
)
325+
326+
# Create root node for main project file
327+
let rootPair = c.toPair(projectFile)
328+
let rootNode = Node(files: @[rootPair], id: 0)
329+
c.nodes.add rootNode
330+
c.processedModules[rootPair.modname] = 0
331+
332+
# Process dependencies
333+
traverseDeps(c, rootPair, rootNode)
334+
335+
# Generate build file
336+
let buildFile = generateBuildFile(c)
337+
rawMessage(conf, hintSuccess, "generated: " & buildFile)
338+
rawMessage(conf, hintSuccess, "run: nifmake run " & buildFile)
339+
else:
340+
rawMessage(conf, errGenerated, "nim deps not available in bootstrap build")

compiler/main.nim

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import pipelines
3434

3535
when not defined(nimKochBootstrap):
3636
import nifbackend
37+
import deps
3738

3839
when not defined(leanCompiler):
3940
import docgen
@@ -447,6 +448,13 @@ proc mainCommand*(graph: ModuleGraph) =
447448
# Generate C code from NIF files
448449
wantMainModule(conf)
449450
commandNifC(graph)
451+
of cmdDeps:
452+
# Generate .build.nif for nifmake
453+
wantMainModule(conf)
454+
when not defined(nimKochBootstrap):
455+
commandDeps(conf)
456+
else:
457+
rawMessage(conf, errGenerated, "nim deps not available in bootstrap build")
450458
of cmdParse:
451459
wantMainModule(conf)
452460
discard parseFile(conf.projectMainIdx, cache, conf)

compiler/options.nim

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ type
176176
# old unused: cmdInterpret, cmdDef: def feature (find definition for IDEs)
177177
cmdCompileToNif
178178
cmdNifC # generate C code from NIF files
179+
cmdDeps # generate .build.nif for nifmake
179180

180181
const
181182
cmdBackends* = {cmdCompileToC, cmdCompileToCpp, cmdCompileToOC,

0 commit comments

Comments
 (0)