summary refs log tree commit diff stats
path: root/tools/atlas
diff options
context:
space:
mode:
authorAndreas Rumpf <rumpf_a@web.de>2023-05-25 22:23:07 +0200
committerGitHub <noreply@github.com>2023-05-25 22:23:07 +0200
commit0eb508e43405662eaddf113aba171119623d6bdb (patch)
tree0a99702dead6b449ac1307d2eba68cb64144813e /tools/atlas
parenta8718d8a9e1aba7df55b1a3df1ce48a3f4f62bff (diff)
downloadNim-0eb508e43405662eaddf113aba171119623d6bdb.tar.gz
atlas: better docs (#21911)
* atlas: better docs

* better workspace/project handling

* make tests green again

* bugfix
Diffstat (limited to 'tools/atlas')
-rw-r--r--tools/atlas/atlas.md72
-rw-r--r--tools/atlas/atlas.nim160
2 files changed, 156 insertions, 76 deletions
diff --git a/tools/atlas/atlas.md b/tools/atlas/atlas.md
index 40cf6411e..c898bc00e 100644
--- a/tools/atlas/atlas.md
+++ b/tools/atlas/atlas.md
@@ -7,6 +7,55 @@ Atlas is compatible with Nimble in the sense that it supports the Nimble
 file format.
 
 
+## Concepts
+
+Atlas uses three concepts:
+
+1. Workspaces
+2. Projects
+3. Dependencies
+
+### Workspaces
+
+Every workspace is isolated, nothing is shared between workspaces.
+A workspace is a directory that has a file `atlas.workspace` inside it. If `atlas`
+is run on a (sub-)directory that is not within a workspace, a workspace is created
+automatically for you. Atlas picks the current directory or one of its parent directories
+that has no `.git` subdirectory inside it as its workspace.
+
+Thanks to this setup, it's easy to develop multiple projects at the same time.
+
+A project plus its dependencies are stored in a workspace:
+
+  $workspace / main project
+  $workspace / _deps / dependency A
+  $workspace / _deps / dependency B
+
+The deps directory can be set via `--deps:DIR` explicitly. It defaults to `_deps`.
+If you want it to be the same as the workspace use `--deps:.`.
+
+
+### Projects
+
+A workspace contains one or multiple "projects". These projects can use each other and it
+is easy to develop multiple projects at the same time.
+
+### Dependencies
+
+Inside a workspace there can be a `_deps` directory where your dependencies are kept. It is
+easy to move a dependency one level up and out the `_deps` directory, turning it into a project.
+Likewise, you can move a project to the `_deps` directory, turning it into a dependency.
+
+The only distinction between a project and a dependency is its location. For dependency resolution
+a project always has a higher priority than a dependency.
+
+
+## No magic
+
+Atlas works by managing two files for you, the `project.nimble` file and the `nim.cfg` file. You can
+edit these manually too, Atlas doesn't touch what should be left untouched.
+
+
 ## How it works
 
 Atlas uses git commits internally; version requirements are translated
@@ -31,29 +80,6 @@ The version selection is deterministic, it picks up the *minimum* required
 version. Thanks to this design, lock files are much less important.
 
 
-## Dependencies
-
-Dependencies are neither installed globally, nor locally into the current
-project. Instead a "workspace" is used. The workspace is the nearest parent
-directory of the current directory that does not contain a `.git` subdirectory.
-Dependencies are managed as **siblings**, not as children. Dependencies are
-kept as git repositories.
-
-Thanks to this setup, it's easy to develop multiple projects at the same time.
-
-A project plus its dependencies are stored in a workspace:
-
-  $workspace / main project
-  $workspace / _deps / dependency A
-  $workspace / _deps / dependency B
-
-The deps directory can be set via `--deps:DIR` explicitly. It defaults to `_deps`.
-If you want it to be the same as the workspace use `--deps:.`.
-
-You can move a dependency out of the `_deps` subdirectory into the workspace.
-This can be convenient should you decide to work on a dependency too. You need to
-patch the `nim.cfg` then.
-
 
 ## Commands
 
diff --git a/tools/atlas/atlas.nim b/tools/atlas/atlas.nim
index ddfbbd086..c3d942d22 100644
--- a/tools/atlas/atlas.nim
+++ b/tools/atlas/atlas.nim
@@ -9,20 +9,26 @@
 ## Simple tool to automate frequent workflows: Can "clone"
 ## a Nimble dependency and its dependencies recursively.
 
-import std / [parseopt, strutils, os, osproc, tables, sets, json, jsonutils]
+import std / [parseopt, strutils, os, osproc, tables, sets, json, jsonutils,
+  parsecfg, streams]
 import parse_requires, osutils, packagesjson
 
 from unicode import nil
 
 const
-  Version = "0.3"
+  Version = "0.4"
   LockFileName = "atlas.lock"
+  AtlasWorkspace = "atlas.workspace"
   Usage = "atlas - Nim Package Cloner Version " & Version & """
 
   (c) 2021 Andreas Rumpf
 Usage:
   atlas [options] [command] [arguments]
 Command:
+  init                  initializes the current directory as a workspace
+    --deps=DIR          use DIR as the directory for dependencies
+                        (default: store directly in the workspace)
+
   use url|pkgname       clone a package and all of its dependencies and make
                         it importable for the current project
   clone url|pkgname     clone a package and all of its dependencies
@@ -31,8 +37,11 @@ Command:
   search keyw keywB...  search for package that contains the given keywords
   extract file.nimble   extract the requirements and custom commands from
                         the given Nimble file
-  updateWorkspace [filter]
-                        update every package in the workspace that has a remote
+  updateProjects [filter]
+                        update every project that has a remote
+                        URL that matches `filter` if a filter is given
+  updateDeps [filter]
+                        update every dependency that has a remote
                         URL that matches `filter` if a filter is given
   build|test|doc|tasks  currently delegates to `nimble build|test|doc`
   task <taskname>       currently delegates to `nimble <taskname>`
@@ -42,9 +51,6 @@ Options:
   --cfgHere             also create/maintain a nim.cfg in the current
                         working directory
   --workspace=DIR       use DIR as workspace
-  --deps=DIR            store dependencies in DIR instead of the workspace
-                        (if DIR is a relative path, it is interpreted to
-                        be relative to the workspace)
   --genlock             generate a lock file (use with `clone` and `update`)
   --uselock             use the lock file for the build
   --version             show the version
@@ -323,11 +329,13 @@ proc commitFromLockFile(c: var AtlasContext; dir: string): string =
   else:
     error c, PackageName(d), "package is not listed in the lock file"
 
-proc checkoutCommit(c: var AtlasContext; w: Dependency) =
-  var dir = c.workspace / w.name.string
-  if not dirExists(dir):
-    dir = c.depsDir / w.name.string
+proc dependencyDir(c: AtlasContext; w: Dependency): string =
+  result = c.workspace / w.name.string
+  if not dirExists(result):
+    result = c.depsDir / w.name.string
 
+proc checkoutCommit(c: var AtlasContext; w: Dependency) =
+  let dir = dependencyDir(c, w)
   withDir c, dir:
     if c.lockOption == genLock:
       genLockEntry(c, w, dir)
@@ -369,10 +377,11 @@ proc findNimbleFile(c: AtlasContext; dep: Dependency): string =
     result = TestsDir / dep.name.string & ".nimble"
     doAssert fileExists(result), "file does not exist " & result
   else:
-    result = c.workspace / dep.name.string / (dep.name.string & ".nimble")
+    let dir = dependencyDir(c, dep)
+    result = dir / (dep.name.string & ".nimble")
     if not fileExists(result):
       result = ""
-      for x in walkFiles(c.workspace / dep.name.string / "*.nimble"):
+      for x in walkFiles(dir / "*.nimble"):
         if result.len == 0:
           result = x
         else:
@@ -531,7 +540,7 @@ proc installDependencies(c: var AtlasContext; nimbleFile: string) =
   let paths = cloneLoop(c, work, startIsDep = true)
   patchNimCfg(c, paths, if c.cfgHere: getCurrentDir() else: findSrcDir(c))
 
-proc updateWorkspace(c: var AtlasContext; dir, filter: string) =
+proc updateDir(c: var AtlasContext; dir, filter: string) =
   for kind, file in walkDir(dir):
     if kind == pcDir and dirExists(file / ".git"):
       c.withDir file:
@@ -617,6 +626,58 @@ proc patchNimbleFile(c: var AtlasContext; dep: string; deps: var seq[string]) =
     else:
       message(c, "[Info] ", toName(thisProject), "up to date: " & nimbleFile)
 
+proc detectWorkspace(): string =
+  result = getCurrentDir()
+  while result.len > 0:
+    if fileExists(result / AtlasWorkspace):
+      return result
+    result = result.parentDir()
+
+proc absoluteDepsDir(workspace, value: string): string =
+  if value == ".":
+    result = workspace
+  elif isAbsolute(value):
+    result = value
+  else:
+    result = workspace / value
+
+when MockupRun:
+  proc autoWorkspace(): string =
+    result = getCurrentDir()
+    while result.len > 0 and dirExists(result / ".git"):
+      result = result.parentDir()
+
+proc createWorkspaceIn(workspace, depsDir: string) =
+  if not fileExists(workspace / AtlasWorkspace):
+    writeFile workspace / AtlasWorkspace, "deps=\"$#\"" % escape(depsDir, "", "")
+  createDir absoluteDepsDir(workspace, depsDir)
+
+proc readConfig(c: var AtlasContext) =
+  let configFile = c.workspace / AtlasWorkspace
+  var f = newFileStream(configFile, fmRead)
+  if f == nil:
+    error c, toName(configFile), "cannot open: " & configFile
+    return
+  var p: CfgParser
+  open(p, f, configFile)
+  while true:
+    var e = next(p)
+    case e.kind
+    of cfgEof: break
+    of cfgSectionStart:
+      discard "who cares about sections"
+    of cfgKeyValuePair:
+      case e.key.normalize
+      of "deps":
+        c.depsDir = absoluteDepsDir(c.workspace, e.value)
+      else:
+        warn c, toName(configFile), "ignored unknown setting: " & e.key
+    of cfgOption:
+      discard "who cares about options"
+    of cfgError:
+      error c, toName(configFile), e.msg
+  close(p)
+
 proc main =
   var action = ""
   var args: seq[string] = @[]
@@ -628,6 +689,11 @@ proc main =
     if args.len != 0:
       error action & " command takes no arguments"
 
+  template projectCmd() =
+    if getCurrentDir() == c.workspace or getCurrentDir() == c.depsDir:
+      error action & " command must be executed in a project, not in the workspace"
+      return
+
   var c = AtlasContext(
     projectDir: getCurrentDir(),
     workspace: "")
@@ -645,9 +711,13 @@ proc main =
       of "version", "v": writeVersion()
       of "keepcommits": c.keepCommits = true
       of "workspace":
-        if val.len > 0:
+        if val == ".":
+          c.workspace = getCurrentDir()
+          createWorkspaceIn c.workspace, c.depsDir
+        elif val.len > 0:
           c.workspace = val
           createDir(val)
+          createWorkspaceIn c.workspace, c.depsDir
         else:
           writeHelp()
       of "deps":
@@ -671,28 +741,27 @@ proc main =
 
   if c.workspace.len > 0:
     if not dirExists(c.workspace): error "Workspace directory '" & c.workspace & "' not found."
-  else:
-    c.workspace = getCurrentDir()
-    while c.workspace.len > 0 and dirExists(c.workspace / ".git"):
-      c.workspace = c.workspace.parentDir()
+  elif action != "init":
+    when MockupRun:
+      c.workspace = autoWorkspace()
+    else:
+      c.workspace = detectWorkspace()
+      if c.workspace.len > 0:
+        readConfig c
+      else:
+        error "No workspace found. Run `atlas init` if you want this current directory to be your workspace."
+        return
+      echo "Using workspace ", c.workspace
 
   when MockupRun:
     c.depsDir = c.workspace
-  else:
-    if c.depsDir.len > 0:
-      if c.depsDir == ".":
-        c.depsDir = c.workspace
-      elif not isAbsolute(c.depsDir):
-        c.depsDir = c.workspace / c.depsDir
-    else:
-      c.depsDir = c.workspace / "_deps"
-    createDir(c.depsDir)
-
-  echo "Using workspace ", c.workspace
 
   case action
   of "":
     error "No action."
+  of "init":
+    c.workspace = getCurrentDir()
+    createWorkspaceIn c.workspace, c.depsDir
   of "clone", "update":
     singleArg()
     let deps = clone(c, args[0], startIsDep = false)
@@ -704,6 +773,7 @@ proc main =
       if c.errors > 0:
         error "There were problems."
   of "use":
+    projectCmd()
     singleArg()
     discard clone(c, args[0], startIsDep = true)
     var deps: seq[string] = @[]
@@ -712,6 +782,7 @@ proc main =
     if c.errors > 0:
       error "There were problems."
   of "install":
+    projectCmd()
     if args.len > 1:
       error "install command takes a single argument"
     var nimbleFile = ""
@@ -730,9 +801,10 @@ proc main =
   of "search", "list":
     updatePackages(c)
     search getPackages(c.workspace), args
-  of "updateworkspace":
-    updateWorkspace(c, c.workspace, if args.len == 0: "" else: args[0])
-    updateWorkspace(c, c.depsDir, if args.len == 0: "" else: args[0])
+  of "updateprojects":
+    updateDir(c, c.workspace, if args.len == 0: "" else: args[0])
+  of "updatedeps":
+    updateDir(c, c.depsDir, if args.len == 0: "" else: args[0])
   of "extract":
     singleArg()
     if fileExists(args[0]):
@@ -740,8 +812,10 @@ proc main =
     else:
       error "File does not exist: " & args[0]
   of "build", "test", "doc", "tasks":
+    projectCmd()
     nimbleExec(action, args)
   of "task":
+    projectCmd()
     nimbleExec("", args)
   else:
     error "Invalid action: " & action
@@ -749,23 +823,3 @@ proc main =
 when isMainModule:
   main()
 
-when false:
-  # some testing code for the `patchNimCfg` logic:
-  var c = AtlasContext(
-    projectDir: getCurrentDir(),
-    workspace: getCurrentDir().parentDir)
-
-  patchNimCfg(c, @[PackageName"abc", PackageName"xyz"])
-
-when false:
-  assert sameVersionAs("v0.2.0", "0.2.0")
-  assert sameVersionAs("v1", "1")
-
-  assert sameVersionAs("1.90", "1.90")
-
-  assert sameVersionAs("v1.2.3-zuzu", "1.2.3")
-  assert sameVersionAs("foo-1.2.3.4", "1.2.3.4")
-
-  assert not sameVersionAs("foo-1.2.3.4", "1.2.3")
-  assert not sameVersionAs("foo", "1.2.3")
-  assert not sameVersionAs("", "1.2.3")