# # # Nim's Runtime Library # (c) Copyright 2015 Andreas Rumpf # # See the file "copying.txt", included in this # distribution, for details about the copyright. # ## This module contains basic operating system facilities like ## retrieving environment variables, working with directories, ## running shell commands, etc. ## .. importdoc:: symlinks.nim, appdirs.nim, dirs.nim, ospaths2.nim runnableExamples: let myFile = "/path/to/my/file.nim" assert splitPath(myFile) == (head: "/path/to/my", tail: "file.nim") when defined(posix): assert parentDir(myFile) == "/path/to/my" assert splitFile(myFile) == (dir: "/path/to/my", name: "file", ext: ".nim") assert myFile.changeFileExt("c") == "/path/to/my/file.c" ## **See also:** ## * `paths `_ and `files `_ modules for high-level file manipulation ## * `osproc module `_ for process communication beyond ## `execShellCmd proc`_ ## * `uri module `_ ## * `distros module `_ ## * `dynlib module `_ ## * `streams module `_ import std/private/ospaths2 export ospaths2 import std/private/osfiles export osfiles import std/private/osdirs export osdirs import std/private/ossymlinks export ossymlinks import std/private/osappdirs export osappdirs import std/private/oscommon include system/inclrtl import std/private/since import std/cmdline export cmdline import std/[strutils, pathnorm] when defined(nimPreviewSlimSystem): import std/[syncio, assertions, widestrs] const weirdTarget = defined(nimscript) or defined(js) since (1, 1): const invalidFilenameChars* = {'/', '\\', ':', '*', '?', '"', '<', '>', '|', '^', '\0'} ## \ ## Characters that may produce invalid filenames across Linux, Windows and Mac. ## You can check if your filename contains any of these chars and strip them for safety. ## Mac bans ``':'``, Linux bans ``'/'``, Windows bans all others. invalidFilenames* = [ "CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"] ## \ ## Filenames that may be invalid across Linux, Windows, Mac, etc. ## You can check if your filename match these and rename it for safety ## (Currently all invalid filenames are from Windows only). when weirdTarget: discard elif defined(windows): import std/[winlean, times] elif defined(posix): import std/[posix, times] proc toTime(ts: Timespec): times.Time {.inline.} = result = initTime(ts.tv_sec.int64, ts.tv_nsec.int) else: {.error: "OS module not ported to your operating system!".} when weirdTarget: {.pragma: noWeirdTarget, error: "this proc is not available on the NimScript/js target".} else: {.pragma: noWeirdTarget.} when defined(nimscript): # for procs already defined in scriptconfig.nim template noNimJs(body): untyped = discard elif defined(js): {.pragma: noNimJs, error: "this proc is not available on the js target".} else: {.pragma: noNimJs.} import std/oserrors export oserrors import std/envvars export envvars import std/private/osseps export osseps proc expandTilde*(path: string): string {. tags: [ReadEnvEffect, ReadIOEffect].} = ## Expands ``~`` or a path starting with ``~/`` to a full path, replacing ## ``~`` with `getHomeDir()`_ (otherwise returns ``path`` unmodified). ## ## Windows: this is still supported despite the Windows platform not having this ## convention; also, both ``~/`` and ``~\`` are handled. ## ## See also: ## * `getHomeDir proc`_ ## * `getConfigDir proc`_ ## * `getTempDir proc`_ ## * `getCurrentDir proc`_ ## * `setCurrentDir proc`_ runnableExamples: assert expandTilde("~" / "appname.cfg") == getHomeDir() / "appname.cfg" assert expandTilde("~/foo/bar") == getHomeDir() / "foo/bar" assert expandTilde("/foo/bar") == "/foo/bar" if len(path) == 0 or path[0] != '~': result = path elif len(path) == 1: result = getHomeDir() elif (path[1] in {DirSep, AltSep}): result = getHomeDir() / path.substr(2) else: # TODO: handle `~bob` and `~bob/` which means home of bob result = path proc quoteShellWindows*(s: string): string {.noSideEffect, rtl, extern: "nosp$1".} = ## Quote `s`, so it can be safely passed to Windows API. ## ## Based on Python's `subprocess.list2cmdline`. ## See `this link `_ ## for more details. let needQuote = {' ', '\t'} in s or s.len == 0 result = "" var backslashBuff = "" if needQuote: result.add("\"") for c in s: if c == '\\': backslashBuff.add(c) elif c == '\"': for i in 0.. 0: result.add(backslashBuff) if needQuote: result.add(backslashBuff) result.add("\"") proc quoteShellPosix*(s: string): string {.noSideEffect, rtl, extern: "nosp$1".} = ## Quote ``s``, so it can be safely passed to POSIX shell. const safeUnixChars = {'%', '+', '-', '.', '/', '_', ':', '=', '@', '0'..'9', 'A'..'Z', 'a'..'z'} if s.len == 0: result = "''" elif s.allCharsInSet(safeUnixChars): result = s else: result = "'" & s.replace("'", "'\"'\"'") & "'" when defined(windows) or defined(posix) or defined(nintendoswitch): proc quoteShell*(s: string): string {.noSideEffect, rtl, extern: "nosp$1".} = ## Quote ``s``, so it can be safely passed to shell. ## ## When on Windows, it calls `quoteShellWindows proc`_. ## Otherwise, calls `quoteShellPosix proc`_. when defined(windows): result = quoteShellWindows(s) else: result = quoteShellPosix(s) proc quoteShellCommand*(args: openArray[string]): string = ## Concatenates and quotes shell arguments `args`. runnableExamples: when defined(posix): assert quoteShellCommand(["aaa", "", "c d"]) == "aaa '' 'c d'" when defined(windows): assert quoteShellCommand(["aaa", "", "c d"]) == "aaa \"\" \"c d\"" # can't use `map` pending https://github.com/nim-lang/Nim/issues/8303 result = "" for i in 0.. 0: result.add " " result.add quoteShell(args[i]) when not weirdTarget: proc c_system(cmd: cstring): cint {. importc: "system", header: "".} when not defined(windows): proc c_free(p: pointer) {. importc: "free", header: "".} const ExeExts* = ## Platform specific file extension for executables. ## On Windows ``["exe", "cmd", "bat"]``, on Posix ``[""]``. when defined(windows): ["exe", "cmd", "bat"] else: [""] proc findExe*(exe: string, followSymlinks: bool = true; extensions: openArray[string]=ExeExts): string {. tags: [ReadDirEffect, ReadEnvEffect, ReadIOEffect], noNimJs.} = ## Searches for `exe` in the current working directory and then ## in directories listed in the ``PATH`` environment variable. ## ## Returns `""` if the `exe` cannot be found. `exe` ## is added the `ExeExts`_ file extensions if it has none. ## ## If the system supports symlinks it also resolves them until it ## meets the actual file. This behavior can be disabled if desired ## by setting `followSymlinks = false`. if exe.len == 0: return template checkCurrentDir() = for ext in extensions: result = addFileExt(exe, ext) if fileExists(result): return when defined(posix): if '/' in exe: checkCurrentDir() else: checkCurrentDir() let path = getEnv("PATH") for candidate in split(path, PathSep): if candidate.len == 0: continue when defined(windows): var x = (if candidate[0] == '"' and candidate[^1] == '"': substr(candidate, 1, candidate.len-2) else: candidate) / exe else: var x = expandTilde(candidate) / exe for ext in extensions: var x = addFileExt(x, ext) if fileExists(x): when not (defined(windows) or defined(nintendoswitch)): while followSymlinks: # doubles as if here if x.symlinkExists: var r = newString(maxSymlinkLen) var len = readlink(x.cstring, r.cstring, maxSymlinkLen) if len < 0: raiseOSError(osLastError(), exe) if len > maxSymlinkLen: r = newString(len+1) len = readlink(x.cstring, r.cstring, len) setLen(r, len) if isAbsolute(r): x = r else: x = parentDir(x) / r else: break return x result = "" when weirdTarget: const times = "fake const" template Time(x: untyped): untyped = string proc getLastModificationTime*(file: string): times.Time {.rtl, extern: "nos$1", noWeirdTarget.} = ## Returns the `file`'s last modification time. ## ## See also: ## * `getLastAccessTime proc`_ ## * `getCreationTime proc`_ ## * `fileNewer proc`_ when defined(posix): var res: Stat if stat(file, res) < 0'i32: raiseOSError(osLastError(), file) result = res.st_mtim.toTime else: var f: WIN32_FIND_DATA var h = findFirstFile(file, f) if h == -1'i32: raiseOSError(osLastError(), file) res
#
#
#           The Nim Compiler
#        (c) Copyright 2018 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

## Basic type definitions the module graph needs in order to support
## incremental compilations.

const nimIncremental* = defined(nimIncremental)

import options, lineinfos, pathutils

when nimIncremental:
  import ast, msgs, intsets, btrees, db_sqlite, std / sha1
  from strutils import parseInt

  type
    Writer* = object
      sstack*: seq[PSym]          # a stack of symbols to process
      tstack*: seq[PType]         # a stack of types to process
      tmarks*, smarks*: IntSet
      forwardedSyms*: seq[PSym]

    Reader* = object
      syms*: BTree[int, PSym]
      types*: BTree[int, PType]

    IncrementalCtx* = object
      db*: DbConn
      w*: Writer
      r*: Reader
      configChanged*: bool

  proc init*(incr: var IncrementalCtx) =
    incr.w.sstack = @[]
    incr.w.tstack = @[]
    incr.w.tmarks = initIntSet()
    incr.w.smarks = initIntSet()
    incr.w.forwardedSyms = @[]
    incr.r.syms = initBTree[int, PSym]()
    incr.r.types = initBTree[int, PType]()


  proc hashFileCached*(conf: ConfigRef; fileIdx: FileIndex; fullpath: AbsoluteFile): string =
    result = msgs.getHash(conf, fileIdx)
    if result.len == 0:
      result = $secureHashFile(string fullpath)
      msgs.setHash(conf, fileIdx, result)

  proc toDbFileId*(incr: var IncrementalCtx; conf: ConfigRef; fileIdx: FileIndex): int =
    if fileIdx == FileIndex(-1): return -1
    let fullpath = toFullPath(conf, fileIdx)
    let row = incr.db.getRow(sql"select id, fullhash from filenames where fullpath = ?",
      fullpath)
    let id = row[0]
    let fullhash = hashFileCached(conf, fileIdx, AbsoluteFile fullpath)
    if id.len == 0:
      result = int incr.db.insertID(sql"insert into filenames(nimid, fullpath, fullhash) values (?, ?, ?)",
        int(fileIdx), fullpath, fullhash)
    else:
      if row[1] != fullhash:
        incr.db.exec(sql"update filenames set fullhash = ? where fullpath = ?", fullhash, fullpath)
      result = parseInt(id)

  proc fromDbFileId*(incr: var IncrementalCtx; conf: ConfigRef; dbId: int): FileIndex =
    if dbId == -1: return FileIndex(-1)
    let fullpath = incr.db.getValue(sql"select fullpath from filenames where id = ?", dbId)
    doAssert fullpath.len > 0, "cannot find file name for DB ID " & $dbId
    result = fileInfoIdx(conf, AbsoluteFile fullpath)


  proc addModuleDep*(incr: var IncrementalCtx; conf: ConfigRef;
                     module, fileIdx: FileIndex;
                     isIncludeFile: bool) =
    if conf.symbolFiles != v2Sf: return

    let a = toDbFileId(incr, conf, module)
    let b = toDbFileId(incr, conf, fileIdx)

    incr.db.exec(sql"insert into deps(module, dependency, isIncludeFile) values (?, ?, ?)",
      a, b, ord(isIncludeFile))

  # --------------- Database model ---------------------------------------------

  proc createDb*(db: DbConn) =
    db.exec(sql"""
      create table if not exists controlblock(
        idgen integer not null
      );
    """)

    db.exec(sql"""
      create table if not exists config(
        config varchar(8000) not null