atlas: better docs (#21911)

* atlas: better docs

* better workspace/project handling

* make tests green again

* bugfix
This commit is contained in:
Andreas Rumpf
2023-05-25 22:23:07 +02:00
committed by GitHub
parent a8718d8a9e
commit 0eb508e434
2 changed files with 156 additions and 76 deletions

View File

@@ -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

View File

@@ -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")