├── .bin
├── bootstrap
├── install
├── install-links
├── shell-hooks
│ ├── .zshlogin
│ ├── .zshlogout
│ ├── .zshrc
│ ├── README.md
│ ├── bash_profile
│ ├── bashrc
│ ├── fish
│ ├── install
│ ├── shell-hooks.sh
│ ├── zsh-login.zsh
│ ├── zsh-logout.zsh
│ └── zshenv
├── uninstall
├── xg.fish
├── xpkg-bash
└── xpkg-dev
├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ ├── Configure.xcscheme
│ ├── XPkg-Package.xcscheme
│ └── xpkg.xcscheme
├── .travis.yml
├── Extras
└── Scripts
├── LICENSE.md
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
├── XPkgCommand
│ └── main.swift
└── XPkgCore
│ ├── Commands
│ ├── CheckCommand.swift
│ ├── InitCommand.swift
│ ├── InstallCommand.swift
│ ├── LinkCommand.swift
│ ├── ListCommand.swift
│ ├── PathCommand.swift
│ ├── RebuildCommand.swift
│ ├── ReinstallCommand.swift
│ ├── RemoveCommand.swift
│ ├── RenameCommand.swift
│ ├── RepairCommand.swift
│ ├── RevealCommand.swift
│ └── UpdateCommand.swift
│ ├── Engine.swift
│ ├── Failure.swift
│ ├── Package.swift
│ └── URL+Extensions.swift
└── Tests
├── LinuxMain.swift
└── XPkgTests
├── XCTestManifests.swift
└── XPkgTests.swift
/.bin/bootstrap:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ROOT="$HOME/.local/share/xpkg"
4 |
5 | if [[ -e "$ROOT/code" ]]
6 | then
7 | echo "Updating xpkg."
8 | cd "$ROOT/code"
9 | git pull
10 | XPKG_INSTALL_MODE="--no-backup"
11 | else
12 | echo "Downloading xpkg."
13 | mkdir -p "$ROOT"
14 | cd "$ROOT"
15 | git clone https://github.com/elegantchaos/XPkg.git code
16 | cd code
17 | fi
18 |
19 | if [[ $? == 0 ]]
20 | then
21 | source "$ROOT/code/.bin/install"
22 | fi
23 |
--------------------------------------------------------------------------------
/.bin/install:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 |
4 | ROOT="$HOME/.local/share/xpkg"
5 |
6 | echo "Building."
7 | swift run builder build
8 | if [[ $? != 0 ]]
9 | then
10 | echo "Build failed."
11 | exit 1
12 | fi
13 |
14 | FISH="$HOME/.config/fish/functions"
15 |
16 | LOCAL="$HOME/.local"
17 | echo "Installing links."
18 | if [[ -e "$LOCAL" ]]
19 | then
20 | echo "Installing to ~/.local."
21 | export STARTUP="$LOCAL/share/shell-hooks/startup"
22 | export BIN="$LOCAL/bin"
23 | export SUDO=
24 | else
25 | echo "Installing to /usr/local (requires sudo)."
26 | export STARTUP="/usr/local/share/shell-hooks/startup"
27 | export BIN="/usr/local/bin"
28 | export SUDO=sudo
29 | fi
30 |
31 | $SUDO mkdir -p "$STARTUP"
32 | $SUDO ln -sf "$ROOT/code/.bin/xpkg-bash" "$STARTUP/xpkg"
33 |
34 | $SUDO mkdir -p "$BIN"
35 | $SUDO ln -sf "$ROOT/code/.build/debug/xpkg" "$BIN/xpkg"
36 | $SUDO ln -sf "$ROOT/code/.bin/xpkg-dev" "$BIN/xpkg-dev"
37 | $SUDO ln -sf "$ROOT/code/.bin/uninstall" "$BIN/xpkg-uninstall"
38 |
39 | mkdir -p "$FISH"
40 | ln -sf "$ROOT/code/.bin/xg.fish" "$FISH/xg.fish"
41 |
42 | echo "Installing shell startup hooks."
43 | $SUDO "$ROOT/code/.bin/shell-hooks/install" "$ROOT" "$XPKG_INSTALL_MODE"
44 |
45 | printf "Done.\n\nOpen a new shell / terminal window and type xpkg to get started.\n\n"
46 |
--------------------------------------------------------------------------------
/.bin/install-links:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 |
4 | ROOT="$HOME/.local/share/xpkg"
5 |
6 | FISH="$HOME/.config/fish/functions"
7 |
8 | LOCAL="$HOME/.local"
9 | echo "Installing links."
10 | if [[ -e "$LOCAL" ]]
11 | then
12 | echo "Installing to ~/.local."
13 | export STARTUP="$LOCAL/share/shell-hooks/startup"
14 | export BIN="$LOCAL/bin"
15 | export SUDO=
16 | else
17 | echo "Installing to /usr/local (requires sudo)."
18 | export STARTUP="/usr/local/share/shell-hooks/startup"
19 | export BIN="/usr/local/bin"
20 | export SUDO=sudo
21 | fi
22 |
23 | $SUDO mkdir -p "$STARTUP"
24 | $SUDO ln -sf "$ROOT/code/.bin/xpkg-bash" "$STARTUP/xpkg"
25 |
26 | $SUDO mkdir -p "$BIN"
27 | $SUDO ln -sf "$ROOT/code/.build/debug/xpkg" "$BIN/xpkg"
28 | $SUDO ln -sf "$ROOT/code/.bin/xpkg-dev" "$BIN/xpkg-dev"
29 | $SUDO ln -sf "$ROOT/code/.bin/uninstall" "$BIN/xpkg-uninstall"
30 |
31 | mkdir -p "$FISH"
32 | ln -sf "$ROOT/code/.bin/xg.fish" "$FISH/xg.fish"
--------------------------------------------------------------------------------
/.bin/shell-hooks/.zshlogin:
--------------------------------------------------------------------------------
1 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | # Created by Sam Deane, 05/06/2019.
3 | # All code (c) 2019 - present day, Elegant Chaos Limited.
4 | # For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | echo "login"
8 |
9 | if [[ "$SHELL_HOOKS_LOGIN" == "" ]]
10 | then
11 |
12 | export SHELL_HOOKS_LOGIN=1
13 | export BASH_HOOKS_LOGIN=1 # legacy
14 |
15 | source_hooks "$SHELL_HOOKS_ROOT/login"
16 | source_hooks "$SHELL_HOOKS_ROOT/login-$SHELL_HOOKS_PLATFORM"
17 |
18 | if [[ -e "$HOME/.zshlogin" ]]
19 | then
20 | source "$HOME/.zshlogin"
21 | fi
22 |
23 | fi
24 |
--------------------------------------------------------------------------------
/.bin/shell-hooks/.zshlogout:
--------------------------------------------------------------------------------
1 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | # Created by Sam Deane, 05/06/2019.
3 | # All code (c) 2019 - present day, Elegant Chaos Limited.
4 | # For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
--------------------------------------------------------------------------------
/.bin/shell-hooks/.zshrc:
--------------------------------------------------------------------------------
1 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | # Created by Sam Deane, 05/06/2019.
3 | # All code (c) 2019 - present day, Elegant Chaos Limited.
4 | # For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | if [[ "$SHELL_HOOKS_RC" == "" ]]
8 | then
9 |
10 | export SHELL_HOOKS_RC=1
11 | export BASH_HOOKS_INTERACTIVE=1 #legacy
12 | source_hooks "$SHELL_HOOKS_ROOT/interactive"
13 | source_hooks "$SHELL_HOOKS_ROOT/interactive-$BASH_HOOKS_PLATFORM"
14 |
15 |
16 | if [[ -e "$HOME/.zshrc" ]]
17 | then
18 | source "$HOME/.zshrc"
19 | fi
20 | fi
21 |
--------------------------------------------------------------------------------
/.bin/shell-hooks/README.md:
--------------------------------------------------------------------------------
1 | These scripts hook into the bash/zsh/fish startup process in a way that's extensible.
2 |
3 | ### BASH
4 |
5 | It replaces the existing `.bashrc` and `.bash_profile` with versions that scan the `~/.config/shell-hooks/startup/` and `~/.config/shell-hooks/login/` folders, and source anything that they find in there.
6 |
7 | They then execute the original `bashrc` and/or `bash_profile` files, which will have been backed up as `~/.bashrc.backup` and `~/.bash_profile.backup` respectively.
8 |
9 | This allows other packages to transparently hook into the bash startup process without having to modify `.bashrc` or `.bash_profile`.
10 |
11 | ### ZSH
12 |
13 | Similar to bash, but replacing the equivalent .zsh* files.
14 |
15 | ### FISH
16 |
17 | Similar to bash, but adds a fish config file.
18 |
--------------------------------------------------------------------------------
/.bin/shell-hooks/bash_profile:
--------------------------------------------------------------------------------
1 | if [[ -e "$HOME/.bashrc" ]]
2 | then
3 | source "$HOME/.bashrc"
4 | fi
5 |
6 | if [[ "$SHELL_HOOKS_LOGIN" == "" ]]
7 | then
8 | export SHELL_HOOKS_LOGIN=1
9 | export BASH_HOOKS_LOGIN=1 # legacy
10 |
11 | source_hooks "$SHELL_HOOKS_ROOT/login"
12 | source_hooks "$SHELL_HOOKS_ROOT/login-$SHELL_HOOKS_PLATFORM"
13 |
14 | if [[ -e "$HOME/.bash_profile.backup" ]]
15 | then
16 | source "$HOME/.bash_profile.backup"
17 | fi
18 | fi
19 |
--------------------------------------------------------------------------------
/.bin/shell-hooks/bashrc:
--------------------------------------------------------------------------------
1 |
2 | if [[ "$SHELL_HOOKS_RC" == "" ]]
3 | then
4 |
5 | export SHELL_HOOKS_SHELL="bash"
6 | export SHELL_HOOKS_RC=1
7 | export SHELL_HOOKS_PLATFORM=`uname`
8 | export SHELL_HOOKS_ROOT="$HOME/.local/share/shell-hooks"
9 |
10 | # legacy
11 | export BASH_HOOKS_RC=1
12 | export BASH_HOOKS_PLATFORM=`uname`
13 | export BASH_HOOKS_ROOT="$HOME/.local/share/shell-hooks"
14 |
15 | # Source a hook
16 | # Before sourcing, we change the working directory to the true
17 | # location of the hook file. This allows hooks to reference other
18 | # local resources just using ./my-resource
19 | function source_hook() {
20 | absolute=$(readlink "$1")
21 | container=$(dirname "$absolute")
22 | pushd "$container" > /dev/null
23 | source "$absolute"
24 | popd > /dev/null
25 | }
26 |
27 | # Source each hook in a folder.
28 | function source_hooks() {
29 | FOLDER=$1
30 | if [[ -e "$FOLDER" ]]
31 | then
32 | for f in "$FOLDER"/*
33 | do
34 | source_hook $f
35 | done
36 | fi
37 | }
38 |
39 | export -f source_hook
40 | export -f source_hooks
41 |
42 | source_hooks "$SHELL_HOOKS_ROOT/startup"
43 | source_hooks "$SHELL_HOOKS_ROOT/startup-$SHELL_HOOKS_PLATFORM"
44 |
45 | if [[ -e "$HOME/.bashrc.backup" ]]
46 | then
47 | source "$HOME/.bashrc.backup"
48 | fi
49 |
50 | # If not running interactively exit now
51 | case $- in
52 | *i*) ;;
53 | *) return;;
54 | esac
55 |
56 | export BASH_HOOKS_INTERACTIVE=1
57 | source_hooks "$SHELL_HOOKS_ROOT/interactive"
58 | source_hooks "$SHELL_HOOKS_ROOT/interactive-$SHELL_HOOKS_PLATFORM"
59 |
60 | fi
61 |
--------------------------------------------------------------------------------
/.bin/shell-hooks/fish:
--------------------------------------------------------------------------------
1 | #!/usr/local/bin/fish
2 |
3 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
4 | # Created by Sam Deane on 26/03/2020.
5 | # All code (c) 2020 - present day, Elegant Chaos Limited.
6 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
7 |
8 | set -x SHELL_HOOKS_SHELL fish
9 | set -x SHELL_HOOKS_RC 1
10 | set -x SHELL_HOOKS_PLATFORM (uname)
11 | set -x SHELL_HOOKS_ROOT "$HOME/.local/share/shell-hooks"
12 | set -x SHELL_HOOKS_INTERACTIVE 1
13 |
14 | if status --is-login
15 | set -x PATH "$HOME/.local/bin" $PATH
16 | end
17 |
--------------------------------------------------------------------------------
/.bin/shell-hooks/install:
--------------------------------------------------------------------------------
1 |
2 | function install_link() {
3 | if [[ "$ROOT" == "uninstall" ]]
4 | then
5 | echo "Removing $2."
6 | rm "$2"
7 | else
8 | if [[ "$3" != "--no-backup" ]]
9 | then
10 | if [[ -e "$2" ]]
11 | then
12 | if [[ ! -e "$2.backup" ]]
13 | then
14 | echo "Backing up $2."
15 | mv "$2" "$2.backup"
16 | fi
17 | rm "$2"
18 | fi
19 | fi
20 |
21 | absolute="$ROOT/code/.bin/shell-hooks/$1"
22 | echo "Installed hook for $(basename $2)."
23 | mkdir -p "$(dirname $2)"
24 | ln -sf "$absolute" "$2"
25 | fi
26 |
27 | }
28 |
29 | #set -e
30 | ROOT=$1
31 | BACKUPSWITCH=$2
32 |
33 | if [[ (! -e "$ROOT") && ("$ROOT" != "uninstall") ]]
34 | then
35 | echo "Root directory is missing. $ROOT"
36 | exit 1
37 | fi
38 |
39 | install_link "bashrc" ~/.bashrc $BACKUPSWITCH
40 | install_link "bash_profile" ~/.bash_profile $BACKUPSWITCH
41 | install_link "zshenv" ~/.zshenv $BACKUPSWITCH
42 |
43 | install_link "shell-hooks.sh" ~/.local/bin/hooks --no-backup
44 | install_link "fish" ~/.config/fish/conf.d/com.elegantchaos.xpkg.fish --no-backup
45 |
--------------------------------------------------------------------------------
/.bin/shell-hooks/shell-hooks.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | list_hooks () {
4 | hooks="$SHELL_HOOKS_ROOT/$1"
5 | if [[ -e "$hooks" ]]
6 | then
7 | files=$(find "$hooks/"* -exec basename {} \;)
8 | fi
9 |
10 | if [[ -e "$hooks-$SHELL_HOOKS_PLATFORM" ]]
11 | then
12 | platform=$(find "$hooks-$SHELL_HOOKS_PLATFORM/"* -exec basename {} \;)
13 | fi
14 |
15 | if [[ "$files$platform" != "" ]]
16 | then
17 | echo "$1:" $files $platform
18 | fi
19 | }
20 |
21 | if [[ "$1" == "list" ]]
22 | then
23 | list_hooks "login"
24 | list_hooks "startup"
25 | list_hooks "interactive"
26 | list_hooks "fish"
27 | else
28 | cmd=$(basename "$0")
29 | echo "Usage: $cmd list"
30 | fi
31 |
--------------------------------------------------------------------------------
/.bin/shell-hooks/zsh-login.zsh:
--------------------------------------------------------------------------------
1 | .zshlogin
--------------------------------------------------------------------------------
/.bin/shell-hooks/zsh-logout.zsh:
--------------------------------------------------------------------------------
1 | .zshlogout
--------------------------------------------------------------------------------
/.bin/shell-hooks/zshenv:
--------------------------------------------------------------------------------
1 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | # Created by Sam Deane, 05/06/2019.
3 | # All code (c) 2019 - present day, Elegant Chaos Limited.
4 | # For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 |
8 | if [[ "$SHELL_HOOKS_ZSH" == "" ]]
9 | then
10 |
11 | export PATH="$HOME/.local/bin":"$PATH"
12 |
13 | export SHELL_HOOKS_ZSH=1
14 | export SHELL_HOOKS_PLATFORM=`uname`
15 | export SHELL_HOOKS_ROOT="$HOME/.local/share/shell-hooks"
16 | export SHELL_HOOKS_SHELL="zsh"
17 |
18 | # legacy
19 | export BASH_HOOKS_RC=1
20 | export BASH_HOOKS_PLATFORM=`uname`
21 | export BASH_HOOKS_ROOT="$HOME/.local/share/shell-hooks"
22 |
23 | # read the other configuration files from here
24 | export ZDOTDIR="$HOME/.local/share/xpkg/code/.bin/shell-hooks"
25 |
26 | # Source a hook
27 | # Before sourcing, we change the working directory to the true
28 | # location of the hook file. This allows hooks to reference other
29 | # local resources just using ./my-resource
30 | export function source_hook() {
31 | absolute=$(readlink "$1")
32 | container=$(dirname "$absolute")
33 | pushd "$container" > /dev/null
34 | source "$absolute"
35 | popd > /dev/null
36 | }
37 |
38 | # Source each hook in a folder.
39 | export function source_hooks() {
40 | FOLDER=$1
41 | if [[ -e "$FOLDER" ]]
42 | then
43 | for f in "$FOLDER"/*
44 | do
45 | source_hook $f
46 | done
47 | fi
48 | }
49 |
50 | source_hooks "$SHELL_HOOKS_ROOT/startup"
51 | source_hooks "$SHELL_HOOKS_ROOT/startup-$SHELL_HOOKS_PLATFORM"
52 |
53 | if [[ -e "$HOME/.zshenv.backup" ]]
54 | then
55 | source "$HOME/.zshenv.backup"
56 | fi
57 |
58 | fi
59 |
--------------------------------------------------------------------------------
/.bin/uninstall:
--------------------------------------------------------------------------------
1 |
2 | ROOT="$HOME/.local/share/xpkg"
3 |
4 | echo "Removing shell startup scripts."
5 | $SUDO "$ROOT/code/.bin/shell-hooks/install" "uninstall"
6 |
7 | echo "Removing shell hooks."
8 | rm -rf ~/.local/share/shell-hooks
9 |
10 | echo "Removing xpkg."
11 | rm -rf ~/.local/share/xpkg
12 |
13 | echo "Done."
14 |
--------------------------------------------------------------------------------
/.bin/xg.fish:
--------------------------------------------------------------------------------
1 | complete --no-files --command xg -n "not __fish_seen_subcommand_from (__fish_xg_complete_packages)" --arguments "(__fish_xg_complete_packages)"
2 |
3 | function __fish_xg_complete_packages
4 | xpkg list --compact
5 | end
6 |
7 | function xg
8 | set -l directory (xpkg path $argv)
9 | pushd $directory
10 | end
11 |
--------------------------------------------------------------------------------
/.bin/xpkg-bash:
--------------------------------------------------------------------------------
1 | xg() {
2 | builtin pushd "`xpkg path $@`"
3 | }
4 |
5 | _comp_xg() {
6 | cur="${COMP_WORDS[COMP_CWORD]}"
7 | COMPREPLY=( $(compgen -W "$(xpkg list --compact)" -- "$cur") )
8 | }
9 |
10 | _comp_xg_zsh() {
11 | # _arguments "1: :($(xpkg list --compact))"
12 | # args = ( "blah: test thing" )
13 | _arguments $args
14 | }
15 |
16 | if [[ "SHELL_HOOKS_SHELL" == "bash" ]]
17 | then
18 | complete -o nospace -o bashdefault -F _comp_xg xg
19 |
20 | elif [[ "SHELL_HOOKS_SHELL" == "zsh" ]]
21 | then
22 | compdef _comp_xg_zsh xg
23 | fi
24 |
--------------------------------------------------------------------------------
/.bin/xpkg-dev:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ROOT="$HOME/.local/share/xpkg"
4 | if [[ ! -e "$ROOT" ]]
5 | then
6 | ROOT="/usr/local/share/xpkg"
7 | fi
8 |
9 | swift run --package-path "$ROOT/code" xpkg "$@"
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata
5 | .bin/shell-hooks/.zcompdump
6 | .bin/shell-hooks/.zsh_history
7 | .bin/shell-hooks/.zsh_sessions
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/Configure.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
57 |
58 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/XPkg-Package.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
43 |
49 |
50 |
51 |
57 |
63 |
64 |
65 |
71 |
77 |
78 |
79 |
80 |
81 |
86 |
87 |
89 |
95 |
96 |
97 |
98 |
99 |
109 |
110 |
116 |
117 |
118 |
119 |
125 |
126 |
132 |
133 |
134 |
135 |
137 |
138 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/xpkg.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
68 |
70 |
76 |
77 |
78 |
79 |
82 |
83 |
86 |
87 |
90 |
91 |
94 |
95 |
98 |
99 |
102 |
103 |
106 |
107 |
110 |
111 |
112 |
113 |
119 |
121 |
127 |
128 |
129 |
130 |
132 |
133 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 |
2 | matrix:
3 | include:
4 | - os: linux
5 | dist: bionic
6 | language: generic
7 | sudo: required
8 | install:
9 | - SWIFT_BRANCH=swift-5.1-release
10 | - SWIFT_VERSION=swift-5.1-RELEASE
11 | - sudo apt-get install clang libicu-dev
12 | - mkdir swift
13 | - curl https://swift.org/builds/$SWIFT_BRANCH/ubuntu1804/$SWIFT_VERSION/$SWIFT_VERSION-ubuntu18.04.tar.gz -s | tar xz -C swift &> /dev/null
14 | - export PATH="$(pwd)/swift/$SWIFT_VERSION-ubuntu18.04/usr/bin:$PATH"
15 | script:
16 | - swift build
17 | - .build/debug/xpkg --version
18 | - swift test
19 | - os: osx
20 | osx_image: xcode11
21 | language: swift
22 | install:
23 | - sudo gem install xcpretty-travis-formatter
24 | script:
25 | - swift build --product builder
26 | - .build/debug/builder build
27 | - .build/debug/xpkg --version
28 | - .build/debug/builder test
29 |
30 |
31 | notifications:
32 | email: false
33 | slack:
34 | rooms:
35 | secure: soh6OcjOfQmamDQBFSET9z95ROkk8mhC9DANX0WYMucAeFu+bvm3DJsilU4MauHwmXVr33IeyoF48QNuOUY8o92KfVO787y9lvS06SFcb27Mw9TE36ws7ZYLlODJ90G9rkfeExVuiguiDY+7g3z5DonVyJ07CROYijrv3x0RB0pVV8ShK6OHYUNPnB5Ce9nDdglIRNYNb9zytT4qzj2X0TySQspEMQosdwbWktbtECE7CK80uLiaKHAa2h3YBaoNvmR9Z2G4VGD/tR8frkV6JhWIMnwm2yA1yWwBi2lT3wtFZ79VRSwkUgRgvb8XhnGUgc0/3+/X7YWK4+fSRMDIIjg+0kTtTuC3XQDrFzBwj7xW2OSEqOMKna5GmwvA/Sspit55BeH4rC49MGWbnEvtksar2E172MCyfH4X2KqjkVytWCkbzY2PdvNqwgFH8lnJfEFEB1Yp+5zeXCepEYUb8yD0Lx97ggrIWEOFfQOIGtpX8FBj/q+oPEeYymvl8FY1oceCt6wAj0YiePFb0XqWqSPFHUwhaNYJ9wb9dgIf1rX51XrGSandpYfjmtJjKb0G5kgP3cMx9OutdqL1wqGWSMdRPd9c+NwoTaZGNfdcao7DjtKYahSzA6f6ZBnRs+CaVLfpBhy1YCRjhCpJyCSJf9EbGkVaJDMBoiuwI2UHQcY=
36 |
--------------------------------------------------------------------------------
/Extras/Scripts:
--------------------------------------------------------------------------------
1 | ../.bin
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Elegant Chaos Limited
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "expressions",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/elegantchaos/Expressions.git",
7 | "state" : {
8 | "revision" : "3f1a4caccb9e33d95d0650e9b78350acd3576300",
9 | "version" : "1.1.1"
10 | }
11 | },
12 | {
13 | "identity" : "files",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/elegantchaos/Files.git",
16 | "state" : {
17 | "revision" : "ccb417b5b4f2d473964594a7c906a50a62c8530c",
18 | "version" : "1.2.2"
19 | }
20 | },
21 | {
22 | "identity" : "logger",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/elegantchaos/Logger.git",
25 | "state" : {
26 | "revision" : "6dc36bcaae47498a79d01a9b41ac5ee6a26e4809",
27 | "version" : "1.8.0"
28 | }
29 | },
30 | {
31 | "identity" : "matchable",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/elegantchaos/Matchable.git",
34 | "state" : {
35 | "revision" : "d53e807009960aafbd54f6229c2542f906628b38",
36 | "version" : "1.0.7"
37 | }
38 | },
39 | {
40 | "identity" : "runner",
41 | "kind" : "remoteSourceControl",
42 | "location" : "https://github.com/elegantchaos/Runner.git",
43 | "state" : {
44 | "revision" : "65420eb4e534a58c8fb1bf18e344bc86edb42c65",
45 | "version" : "1.3.2"
46 | }
47 | },
48 | {
49 | "identity" : "semanticversion",
50 | "kind" : "remoteSourceControl",
51 | "location" : "https://github.com/elegantchaos/SemanticVersion.git",
52 | "state" : {
53 | "revision" : "f6851038d07c1d8dc585754131995654fdcbec43",
54 | "version" : "1.1.1"
55 | }
56 | },
57 | {
58 | "identity" : "swift-argument-parser",
59 | "kind" : "remoteSourceControl",
60 | "location" : "https://github.com/apple/swift-argument-parser",
61 | "state" : {
62 | "revision" : "6b2aa2748a7881eebb9f84fb10c01293e15b52ca",
63 | "version" : "0.5.0"
64 | }
65 | },
66 | {
67 | "identity" : "versionator",
68 | "kind" : "remoteSourceControl",
69 | "location" : "https://github.com/elegantchaos/Versionator.git",
70 | "state" : {
71 | "revision" : "d7bf7747ae277f6a5cd3193806262160d579f0cc",
72 | "version" : "1.0.3"
73 | }
74 | },
75 | {
76 | "identity" : "xctestextensions",
77 | "kind" : "remoteSourceControl",
78 | "location" : "https://github.com/elegantchaos/XCTestExtensions.git",
79 | "state" : {
80 | "revision" : "c1cec43d4ca0b657323080b90a840b675e8af638",
81 | "version" : "1.4.9"
82 | }
83 | },
84 | {
85 | "identity" : "xpkgpackage",
86 | "kind" : "remoteSourceControl",
87 | "location" : "https://github.com/elegantchaos/XPkgPackage.git",
88 | "state" : {
89 | "revision" : "cdcc0cff87cfe9f117c0cd26b700137f25a968de",
90 | "version" : "1.1.1"
91 | }
92 | }
93 | ],
94 | "version" : 2
95 | }
96 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.6
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "XPkg",
7 |
8 | platforms: [
9 | .macOS(.v10_13)
10 | ],
11 |
12 | products: [
13 | .executable(name: "xpkg", targets: ["XPkgCommand"]),
14 | ],
15 |
16 | dependencies: [
17 | .package(url: "https://github.com/elegantchaos/CommandShell.git", from: "2.1.5"),
18 | .package(url: "https://github.com/elegantchaos/Expressions.git", from: "1.1.1"),
19 | .package(url: "https://github.com/elegantchaos/Files.git", from: "1.2.2"),
20 | .package(url: "https://github.com/elegantchaos/Logger.git", from: "1.8.0"),
21 | .package(url: "https://github.com/elegantchaos/Runner.git", from: "1.3.2"),
22 | .package(url: "https://github.com/elegantchaos/SemanticVersion.git", from: "1.1.1"),
23 | .package(url: "https://github.com/elegantchaos/Versionator.git", from: "1.0.3"),
24 | .package(url: "https://github.com/elegantchaos/XPkgPackage.git", from: "1.0.9")
25 | ],
26 |
27 | targets: [
28 | .executableTarget(
29 | name: "XPkgCommand",
30 |
31 | dependencies: [
32 | "XPkgCore"
33 | ],
34 | plugins: [
35 | .plugin(name: "VersionatorPlugin", package: "Versionator")
36 | ]
37 | ),
38 |
39 | .target(
40 | name: "XPkgCore",
41 |
42 | dependencies: [
43 | "CommandShell",
44 | "Expressions",
45 | "Files",
46 | "Logger",
47 | "Runner",
48 | "SemanticVersion",
49 | "XPkgPackage",
50 | ]
51 | ),
52 |
53 | .testTarget(
54 | name: "XPkgTests",
55 | dependencies: [
56 | "XPkgCore"
57 | ]
58 | )
59 | ]
60 | )
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # xpkg
2 |
3 | A simple cross-platform "package" manager.
4 |
5 | ## Overview
6 |
7 | I tend to set up many aspects of my environment in a similar way, whether I'm on Linux, macOS, or some other unix-like.
8 |
9 | As part of this, I have a whole grab-bag of scripts, configuration files, and the like, which I need to somehow download, install, and then quite often sym-link into various locations.
10 |
11 | Previously I've done this by having a single git repo that I clone, and a bunch of scripts within it that I hook up to things like `.bashrc` in order to get everything working.
12 |
13 | This isn't a very scalable solution, and I wanted something better.
14 |
15 | Specifically I wanted to be able to:
16 |
17 | - split the monolithic dump of tools and scripts into individual packages
18 | - have each package live in a git repo
19 | - easily install or remove a package
20 | - have hooks automatically run when a package is installed, to set up symlinks etc
21 | - have similar hooks run automatically to clean up when a package is removed
22 |
23 | In a nutshell, that's what XPkg does.
24 |
25 | ## Installation
26 |
27 | **Disclaimer: XPkg was developed purely for my own use. It is under active (albeit sporadic) development and probably has bugs in it. It will modify your `.bashrc`, `.zshrc` etc scripts, and although it backs them up, it may break things. Please do tell me if it does something wrong, but I can't guarantee to support you, and I definitely won't be held responsible for any damage it does. Use XPkg at your own risk!**
28 |
29 | ### Dependencies
30 |
31 | When bringing up a new machine, Xpkg is one of the first things I install - and I then use it to install lots of other things.
32 |
33 | There one or two things that it does require first, however:
34 |
35 | - git
36 | - swift (or Xcode)
37 | - github access (ideally via ssh, which means you need to set up your ssh keys for the machine and register them with github)
38 |
39 |
40 | ### Bootstrap
41 |
42 | Running `curl https://raw.githubusercontent.com/elegantchaos/XPkg/master/.bin/bootstrap | bash` should get you up and running.
43 |
44 | What this does is:
45 | - clone the project into `.local/share/xpkg`
46 | - build it
47 | - install some hooks to link it in to your path
48 | - prompt you to open a new shell or terminal
49 |
50 | During installation, an alias to xpkg is installed into `~/.local/bin`, and some startup hooks are installed for `bash`, `zsh` and `fish` which include this location in `$PATH`.
51 |
52 | *You need to start a new shell / open a new terminal window before this path change is picked up.*
53 |
54 |
55 |
56 | ## Usage
57 |
58 | To install a package from Github: `xpkg install `.
59 |
60 | This will clone the package into a hidden location, then run its installer.
61 |
62 | You can also specify a full repo URL if it's not on github.
63 |
64 | To remove a package, `xpkg remove `.
65 |
66 | To list the installed packages `xpkg list`.
67 |
68 | To navigate to a package directory (using `pushd`), type `xg `.
69 |
70 | For other commands, see `xpkg help`.
71 |
72 | ## Writing Packages
73 |
74 | At their most basic, packages are just git repositories, which contain a payload (whatever files you want to install or hook into your system), plus an installer (that tells XPkg how to install/uninstall the payload).
75 |
76 | XPkg was designed to be cross-platform (this is what the "X" stands for), and is written in Swift. It should work on any platform that has a working Swift compiler and standard libraries. Since Swift is a requirement for XPkg itself, I decided to also make it a requirement for the installer.[^1]
77 |
78 | In fact, XPkg packages are also Swift Package Manager (SPM) packages.
79 |
80 | When you install a package with XPkg, it clones the corresponding repository, then uses SPM to build and run the installer (a product with a special name, currently `-xpkg-hooks`) in your package, passing it a known set of arguments and environment variables.
81 |
82 | There are two nice aspects of this design choice:
83 |
84 | - Your installer can do whatever it wants when it is run; it's just code!
85 | - We get dependency management for free
86 | - the installer can list other SPM packages as dependencies, and use them when its run
87 | - it can also list other _XPkg_ packages as dependencies; SPM will pull them in, and XPkg will notice that they've been pulled in and install them!
88 |
89 | Of course, there are some things that you commonly want to do when installing a package, such as creating symbolic links to places like `/usr/local/bin`, running other scripts, etc.
90 |
91 | To avoid every installer having to write that code every time, we just put them in another Swift package (currently called `XpkgPackage`) which all installers can import and use.
92 |
93 | [^1]: The first iteration of XPkg didn't have installers, it had manifests. The manifest was a hidden json file called `.xpkg.json` which sat at the root of package and described how to install/uninstall the package. This was lightweight, but a little inflexible, since it was up to XPkg to interpret the manifest. You could run code by invoking shell scripts, but you couldn't specify a requirement for other packages as dependencies.
94 |
95 | [^2]: Note that the package can be a real swift package, designed for use in building Swift products, and with a other products and targets. It doesn't have to be, but it can be.
96 |
97 | ### Example
98 |
99 | A simple package might consist of the following files:
100 |
101 | my-package/
102 | Package.swift
103 | Sources/xpkg-my-package/
104 | main.swift
105 | Payload/
106 | my-command.sh
107 |
108 | The `Package.swift` file might look like this:
109 |
110 | ```Swift
111 |
112 | // swift-tools-version:5.0
113 |
114 | import PackageDescription
115 |
116 | let package = Package(
117 | name: "my-package",
118 | platforms: [
119 | .macOS(.v10_13)
120 | ],
121 | products: [
122 | .executable(name: "my-package-xpkg-hooks", targets: ["my-package-xpkg-hooks"]),
123 | ],
124 | dependencies: [
125 | .package(url: "https://github.com/elegantchaos/XPkgPackage", from:"1.0.5"),
126 | ],
127 | targets: [
128 | .target(
129 | name: "my-package-xpkg-hooks",
130 | dependencies: ["XPkgPackage"]),
131 | ]
132 | )
133 | ```
134 |
135 | The `main.swift` file might look like this:
136 |
137 | ```Swift
138 | import XPkgPackage
139 |
140 | let links = [
141 | ["Payload/my-command.sh"]
142 | ]
143 |
144 | let arguments = CommandLine.arguments
145 | let package = InstalledPackage(fromCommandLine: arguments)
146 | try! package.performAction(fromCommandLine: CommandLine.arguments, links: links, commands: [])
147 | ```
148 |
149 | This uses the functionality provided by XPkgPackage to install a link `my-command` into `/usr/local/bin`, which points to the file `Payload/my-command.sh` in the cached version of the package on the disk.
150 |
151 | ### A Note About Terminology
152 |
153 | XPkg has undergone a major redesign, and the terminology hasn't caught up yet.
154 |
155 | Previously we had packages and manifests. Now we have packages and installers.
156 |
157 | I plan to rename a lot of things to reflect the new reality:
158 |
159 | - `xpkg-hooks` will probably become `xpkg-installer`
160 | - `XPkgPackage` will probably become `XPkgInstaller`
161 |
162 | ### Hooking Into Shell Startup
163 |
164 | Something that many packages need to do is to hook into the shell startup process (be it .bashrc, .zshrc, or whatever), in order to set environment variables, aliases, and so on.
165 |
166 | Rather than have each package modify these init files, which could get messy, I decided to have one package install itself into this startup process, and have this package provide a flexible way to install other hooks.
167 |
168 | This package is called `shell-hooks` (https://github.com/elegantchaos/shell-hooks), and is installed by default when you install XPkg itself.
169 |
170 | It hooks itself into the startup process for Bash, Zsh, and Fish. At startup, it scans `~/.config/shell-hooks/` and runs any files that it finds there. It actually uses subdirectories and pattern matching to choose exactly which files to run, depending on what platform you're on, and whether this is an interative or non-interactive session.
171 |
172 | The standard installer support package `XpkgPackage` knows about shell-hooks, and provides support for installing symbolic links into its directories. This makes it really simple for Xpkg packages to insert themselves into the shell startup process.
173 |
174 |
175 | ## Existing Packages
176 |
177 | I'm slowly converting over my tangle of old scripts and links to packages, and have a bunch that I've made, supporting things like:
178 |
179 | - Atom setup
180 | - Travis helpers
181 | - Git configs and helpers
182 | - Terminal setup
183 | - Xcode templates
184 | - Coding fonts
185 | - Homebrew installation
186 | - Swift helpers
187 | - Mouse helpers (for Linux)
188 | - Tabtab support (for Linux)
189 | - Keyboard support (for Linux)
190 | - VIM settings
191 | - Appledoc helpers
192 | - Conky settings
193 |
194 | Many of these are in private repos, because they're basically _my_ settings, but you'll find a few public on github. I will try to open up more over time, I just need to make sure that they don't accidentally contain private tokens or other stuff not-for-general-consumption.
195 |
196 |
197 | ## How It Works
198 |
199 | XPkg basically creates a local swift package in a hidden directory, and maintains a `Package.swift` file in there.
200 |
201 | When you install a package, XPkg adds it to the `Package.swift` file, and uses `swift update` to resolve it and fetch the dependencies.
202 |
203 | It then spots any packages that got added as a result of this, and tries to build and run the installer for them.
204 |
205 | Package removal works in a similar way, in reverse.
206 |
207 | There's a bit more to it than that, but that's the basic idea.
208 |
209 | Using SPM to do all the dependency resolution and fetching seemed like a good way to get a lot of fuctionality for not a lot of work!
210 |
211 | The idea is potentially pretty solid I think, and would be purely an implementation detail if it weren't for the fact that the use of SPM is exposed as a way to provide the installers. In theory other installer mechanisms could be provided instead / as well.
212 |
213 | During development, I've found that occasionally bugs in XPkg itself can cause the auto-generated `Package.swift` to become corrupted and need hand editing. This is obviously not ideal for something that other people would use, but it should be possible to prevent this corruption from happening when XPkg itself settles down.
214 |
215 |
216 | ## Future Plans
217 |
218 | As well as supporting installation, XPkg was originally intended to help with some other things:
219 |
220 | - installing and navigating to work projects on my machine (eg being able to type `xg MyProject` and cd to my working directory for MyProject)
221 | - automatically fetching / pull a list of tracked git repos (both packages and projects)
222 | - automatically backing up / pushing a list of tracked git repos (both packages and projects)
223 |
224 | This is all intended to help support an existence where you are working on multiple machines at the same time / moving regularly between machines.
225 |
226 | Some of these features are in the pipeline, or may be added at a later date.
227 |
228 | XPkg also used to install itself into `/usr/local/share`, and install links etc into `/usr/local/bin`. At some point I moved over into using `~/.local/` instead. At some point I intend to make it support either, depending on a configuration flag. At some point. Maybe...
229 |
--------------------------------------------------------------------------------
/Sources/XPkgCommand/main.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 08/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import CommandShell
8 | import Foundation
9 | import XPkgCore
10 | import SemanticVersion
11 |
12 | var info: [String:Any] = [:]
13 |
14 | info[.versionInfoKey] = CurrentVersion.string
15 | info[.buildInfoKey] = CurrentVersion.build
16 |
17 | let components = Calendar.current.dateComponents([.year], from: Date())
18 | info[.copyrightInfoKey] = "Copyright © \(components.year!) Elegant Chaos. All rights reserved."
19 |
20 | CommandShell.main(info: info)
21 |
22 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Commands/CheckCommand.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 11/07/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import ArgumentParser
8 | import CommandShell
9 | import Foundation
10 |
11 | public struct CheckCommand: ParsableCommand {
12 | static public var configuration: CommandConfiguration = CommandConfiguration(
13 | commandName: "check",
14 | abstract: "Check that an installed package is ok."
15 | )
16 |
17 | @Argument(help: "The package to check.") var packageName: String
18 | @OptionGroup() var common: CommandShellOptions
19 |
20 | public init() {
21 | }
22 |
23 | public func run() throws {
24 | let engine: Engine = common.loadEngine()
25 | if packageName == "" {
26 | let _ = try engine.forEachPackage { (package) in
27 | check(package: package, engine: engine)
28 | }
29 | } else {
30 | let manifest = try engine.loadManifest()
31 | let package = try engine.existingPackage(from: packageName, manifest: manifest)
32 | check(package: package, engine: engine)
33 | }
34 | }
35 |
36 | func check(package: Package, engine: Engine) {
37 | if package.check(engine: engine) {
38 | engine.output.log("\(package.name) ok.")
39 | } else {
40 | engine.output.log("\(package.name) missing.")
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Commands/InitCommand.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane on 18/07/2019.
3 | // All code (c) 2019 - present day, Elegant Chaos Limited.
4 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
5 |
6 | import ArgumentParser
7 | import CommandShell
8 | import Foundation
9 | import Runner
10 |
11 |
12 | public struct InitCommand: ParsableCommand {
13 | static public var configuration: CommandConfiguration = CommandConfiguration(
14 | commandName: "init",
15 | abstract: "Create a new package."
16 | )
17 |
18 | @OptionGroup() var common: CommandShellOptions
19 |
20 | public init() {
21 | }
22 |
23 | public func run() throws {
24 | let fm = FileManager.default
25 | let engine: Engine = common.loadEngine()
26 | var name = engine.remoteOriginForCwd()
27 | var path = engine.localRepoForCwd()
28 | if path.isEmpty {
29 | path = fm.currentDirectoryPath
30 | }
31 | let root = URL(fileURLWithPath: path)
32 |
33 | if name.isEmpty {
34 | name = root.lastPathComponent
35 | }
36 |
37 | do {
38 | try writeFiles(for: name, to: root, engine: engine)
39 | engine.output.log("Inited package \(name).")
40 | } catch {
41 | engine.output.log("Failed to init package \(name).")
42 | engine.verbose.log(error)
43 | }
44 | }
45 |
46 | fileprivate func writeFiles(for name: String, to root: URL, engine: Engine) throws {
47 | let user = engine.gitUserName() ?? ProcessInfo.processInfo.environment["USER"] ?? "Unknown"
48 | let now = DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .none)
49 | let manifest = manifestSource(for: name, user: user, date: now)
50 | let main = mainSource(for: name, user: user, date: now)
51 | let runner = Runner(for: engine.gitURL)
52 | let fm = FileManager.default
53 |
54 | // create manifest
55 | try manifest.write(to: root.appendingPathComponent("Package.swift"), atomically: true, encoding: .utf8)
56 |
57 | // create source
58 | let source = root.appendingPathComponent("Sources/\(name)-xpkg-hooks")
59 | try? fm.createDirectory(at: source, withIntermediateDirectories: true, attributes: nil)
60 | try main.write(to: source.appendingPathComponent("main.swift"), atomically: true, encoding: .utf8)
61 |
62 | // create gitignore if we've a global one to copy
63 | if let result = try? runner.sync(arguments: ["config", "--global", "core.excludesfile"]) {
64 | if result.status == 0 {
65 | let globalIgnoreURL = URL(fileURLWithPath: result.stdout.trimmingCharacters(in: .newlines))
66 | try? fm.copyItem(at: globalIgnoreURL, to: root.appendingPathComponent(".gitignore"))
67 | }
68 | }
69 |
70 | // git init if necessary
71 | if !fm.fileExists(at: root.appendingPathComponent(".git")) {
72 | let _ = try? runner.sync(arguments: ["init"])
73 | }
74 | }
75 |
76 | func manifestSource(for name: String, user: String, date: String) -> String {
77 | return """
78 | // swift-tools-version:5.0
79 |
80 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
81 | // \(name) - An XPkg package.
82 | // Created by \(user), \(date).
83 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
84 |
85 | import PackageDescription
86 |
87 | let package = Package(
88 | name: "\(name)",
89 | platforms: [
90 | .macOS(.v10_13)
91 | ],
92 | products: [
93 | .executable(name: "\(name)-xpkg-hooks", targets: ["\(name)-xpkg-hooks"]),
94 | ],
95 | dependencies: [
96 | .package(url: "https://github.com/elegantchaos/XPkgPackage", from:"1.0.0"),
97 | ],
98 | targets: [
99 | .target(
100 | name: "\(name)-xpkg-hooks",
101 | dependencies: ["XPkgPackage"]),
102 | ]
103 | )
104 | """
105 | }
106 |
107 | func mainSource(for name: String, user: String, date: String) -> String {
108 | return """
109 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
110 | // \(name) - An XPkg package.
111 | // Created by \(user), \(date).
112 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
113 |
114 | import XPkgPackage
115 |
116 | let links: [InstalledPackage.ManifestLink] = []
117 |
118 | let package = InstalledPackage(fromCommandLine: CommandLine.arguments)
119 | try! package.performAction(fromCommandLine: CommandLine.arguments, links: links)
120 | """
121 |
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Commands/InstallCommand.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 08/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import ArgumentParser
8 | import CommandShell
9 | import Foundation
10 |
11 | public struct InstallCommand: ParsableCommand {
12 | @Argument(help: "The package to install.") var packageSpec: String
13 | @Flag(name: .customLong("project"), help: "Install in the projects folder, and not as a package.") var asProject = false
14 | @Option(name: .customLong("as"), help: "The name to use for the package.") var asName: String?
15 | @OptionGroup() var common: CommandShellOptions
16 |
17 | static public var configuration: CommandConfiguration = CommandConfiguration(
18 | commandName: "install",
19 | abstract: "Install a package."
20 | )
21 |
22 | public init() {
23 | }
24 |
25 | public func run() throws {
26 | let engine: Engine = common.loadEngine()
27 | try engine.install(packageSpec: packageSpec, asProject: asProject, asName: asName)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Commands/LinkCommand.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 08/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import ArgumentParser
8 | import CommandShell
9 | import Foundation
10 | import Runner
11 |
12 | public struct LinkCommand: ParsableCommand {
13 | @Argument(help: "The name of the package to link to.") var packageName: String
14 | @Argument(help: "The path to the package.") var packagePath: String
15 | @OptionGroup() var common: CommandShellOptions
16 |
17 | static public var configuration: CommandConfiguration = CommandConfiguration(
18 | commandName: "link",
19 | abstract: "Link an existing folder as a package."
20 | )
21 |
22 | public init() {
23 | }
24 |
25 | public func run() throws {
26 | let engine: Engine = common.loadEngine()
27 | let output = engine.output
28 | var package = self.packageName
29 | var path = self.packagePath
30 |
31 | if package == "" {
32 | package = engine.remoteOriginForCwd()
33 | }
34 |
35 | if path == "" {
36 | path = engine.localRepoForCwd()
37 | }
38 |
39 | guard package != "", let linkedURL = URL(string: path) else {
40 | output.log("Couldn't infer package name/path.")
41 | return
42 | }
43 |
44 | let packageURL = linkedURL.appendingPathComponent("Package.swift")
45 | if FileManager.default.fileExists(at: packageURL) {
46 | try engine.install(packageSpec: package, linkTo: linkedURL)
47 | } else {
48 | engine.output.log("Linked \(package) as an alias.")
49 | let defaults = UserDefaults.standard
50 | var aliases: [String:String]
51 | if let current = defaults.dictionary(forKey: "aliases") as? [String:String] {
52 | aliases = current
53 | } else {
54 | aliases = [:]
55 | }
56 |
57 | let key = linkedURL.lastPathComponent
58 | aliases[key] = linkedURL.path
59 | defaults.set(aliases, forKey: "aliases")
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Commands/ListCommand.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 13/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import ArgumentParser
8 | import CommandShell
9 | import Foundation
10 |
11 |
12 | public struct ListCommand: ParsableCommand {
13 | @Flag(help: "Produces output on a single line.") var oneline = false
14 | @Flag(help: "Produces minimal output.") var compact = false
15 | @Flag(help: "Produces output with extra details.") var full = false
16 | @OptionGroup() var common: CommandShellOptions
17 |
18 | static public var configuration: CommandConfiguration = CommandConfiguration(
19 | commandName: "list",
20 | abstract: "List the installed packages."
21 | )
22 |
23 | public init() {
24 | }
25 |
26 | public func run() throws {
27 | let engine: Engine = common.loadEngine()
28 | if oneline {
29 | try listOneline(engine: engine)
30 | } else if compact {
31 | try listCompact(engine: engine)
32 | } else if full {
33 | try listFull(engine: engine)
34 | } else {
35 | try listNormal(engine: engine)
36 | }
37 | }
38 |
39 | func listOneline(engine: Engine) throws {
40 | var output: [String] = []
41 | let _ = try engine.forEachPackage { (package) in
42 | output.append(package.name)
43 | }
44 | engine.output.log(output.joined(separator: " "))
45 | }
46 |
47 | func listCompact(engine: Engine) throws {
48 | let gotPackages = try engine.forEachPackage { (package) in
49 | engine.output.log("\(package.name)")
50 | }
51 | if !gotPackages {
52 | engine.output.log("No packages installed.")
53 | }
54 | }
55 |
56 | func listNormal(engine: Engine) throws {
57 | var gotLinked = false
58 | let gotPackages = try engine.forEachPackage { (package) in
59 | let linked = !package.local.absoluteString.contains(engine.vaultURL.absoluteString)
60 | let flags = linked ? "*" : " "
61 | gotLinked = gotLinked || linked
62 | let status = package.status(engine: engine)
63 | let statusString = status == .pristine ? "" : " (\(status))"
64 | engine.output.log("\(flags) \(package.name)\(statusString)")
65 | }
66 |
67 |
68 | if !gotPackages {
69 | engine.output.log("No packages installed.")
70 | } else if gotLinked {
71 | engine.output.log("\n(items marked with * are linked to external folders)")
72 | }
73 | }
74 |
75 | func listFull(engine: Engine) throws {
76 | let gotPackages = try engine.forEachPackage { (package) in
77 | let linked = !package.local.absoluteString.contains(engine.vaultURL.absoluteString)
78 | let location = linked ? "\(package.local.path) (linked)" : package.local.path
79 | let status = package.status(engine: engine)
80 | let version = engine.currentVersion(forLocalURL: package.local) ?? ""
81 | let statusString = status == .pristine ? "" : " (\(status))\n"
82 | engine.output.log("\n\(package.name) (\(version))\n---------------------\n\(package.url)\n\(location)\n\(statusString)")
83 | }
84 |
85 | if !gotPackages {
86 | engine.output.log("No packages installed.")
87 | }
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Commands/PathCommand.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 13/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import ArgumentParser
8 | import CommandShell
9 | import Foundation
10 |
11 | public struct PathCommand: ParsableCommand {
12 | @Argument(help: "The package to show.") var packageName: String?
13 | @Flag(name: .customLong("self"), help: "Perform the action on xpkg itself, rather than an installed package.") var asSelf = false
14 | @Flag(help: "Show the path to the vault.") var vault = false
15 | @Flag(help: "Show the path to the projects folder.") var projects = false
16 | @OptionGroup() var common: CommandShellOptions
17 |
18 | static public var configuration: CommandConfiguration = CommandConfiguration(
19 | commandName: "path",
20 | abstract: "Show the path of a package."
21 | )
22 |
23 | public init() {
24 | }
25 |
26 | public func run() throws {
27 | let engine: Engine = common.loadEngine()
28 | var url: URL? = nil
29 |
30 | if asSelf {
31 | url = engine.xpkgCodeURL
32 | } else if vault {
33 | url = engine.vaultURL
34 | } else if projects {
35 | url = engine.projectsURL
36 | } else if let name = packageName {
37 | if let package = engine.possiblePackage(named: name, manifest: try engine.loadManifest()) {
38 | url = package.local
39 | } else {
40 | let project = engine.projectsURL.appendingPathComponent(name)
41 | if FileManager.default.fileExists(at: project) {
42 | url = project
43 | }
44 | }
45 | } else {
46 | throw ValidationError("Package name required.")
47 | }
48 |
49 | if let found = url {
50 | engine.output.log(found.path)
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Commands/RebuildCommand.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 14/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import ArgumentParser
8 | import Runner
9 | import CommandShell
10 |
11 | public struct RebuildCommand: ParsableCommand {
12 | @OptionGroup() var common: CommandShellOptions
13 |
14 | static public var configuration: CommandConfiguration = CommandConfiguration(
15 | commandName: "rebuild",
16 | abstract: "Rebuild XPkg itself."
17 | )
18 |
19 | public init() {
20 | }
21 |
22 | public func run() throws {
23 | let engine: Engine = common.loadEngine()
24 | try engine.runBootstrap()
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Commands/ReinstallCommand.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 20/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import ArgumentParser
8 | import CommandShell
9 | import Foundation
10 |
11 | public struct ReinstallCommand: ParsableCommand {
12 | @Argument(help: "The package to reinstall.") var packageName: String
13 | @OptionGroup() var common: CommandShellOptions
14 |
15 | static public var configuration: CommandConfiguration = CommandConfiguration(
16 | commandName: "reinstall",
17 | abstract: "Re-install the package. This is the equivalent of doing remove followed by install ."
18 | )
19 |
20 | public init() {
21 | }
22 |
23 | public func run() throws {
24 | let engine: Engine = common.loadEngine()
25 | let manifest = try engine.loadManifest()
26 | let package = try engine.existingPackage(from: packageName, manifest: manifest)
27 |
28 | engine.attempt(action: "Reinstalling \(package.name).") {
29 | do {
30 | engine.verbose.log("Uninstalling \(package.name)")
31 | if try package.run(action: "remove", engine: engine) {
32 | engine.output.log("Removed \(package.name).")
33 | }
34 | engine.verbose.log("Installing \(package.name)")
35 | if try package.run(action: "install", engine: engine) {
36 | engine.output.log("Reinstalled \(package.name).")
37 | }
38 | } catch {
39 | engine.output.log("Reinstall of \(package.name) failed.")
40 | engine.verbose.log(error)
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Commands/RemoveCommand.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 08/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import ArgumentParser
8 | import CommandShell
9 | import Foundation
10 | import Runner
11 |
12 | public struct RemoveCommand: ParsableCommand {
13 | @Argument(help: "The package to remove.") var packageName: String
14 | @Flag(help: "Force the removal of the package even if there are local changes.") var force = false
15 | @OptionGroup() var common: CommandShellOptions
16 |
17 | static public var configuration: CommandConfiguration = CommandConfiguration(
18 | commandName: "remove",
19 | abstract: "Remove a package."
20 | )
21 |
22 | public init() {
23 | }
24 |
25 | public func run() throws {
26 | let engine: Engine = common.loadEngine()
27 | let output = engine.output
28 |
29 | let manifest = try engine.loadManifest()
30 | let package = try engine.existingPackage(from: packageName, manifest: manifest)
31 |
32 | var safeToDelete = force
33 | if !safeToDelete {
34 | switch package.status(engine: engine) {
35 | case .unknown:
36 | output.log("Failed to check \(package.name) status - it might be modified or un-pushed. Use --force to force deletion.")
37 | case .pristine:
38 | safeToDelete = true
39 | case .modified:
40 | output.log("Package \(package.name) is modified. Use --force to force deletion.")
41 | case .uncommitted:
42 | output.log("Package \(package.name) has no commits. Use --force to force deletion.")
43 | case .ahead:
44 | output.log("Package \(package.name) has un-pushed commits. Use --force to force deletion.")
45 | case .untracked:
46 | output.log("Package \(package.name) is not tracking remotely or may have un-pushed commits. Use --force to force deletion.")
47 | }
48 | }
49 |
50 | // try to unlink the package
51 | if safeToDelete {
52 | safeToDelete = package.unedit(engine: engine)
53 | }
54 |
55 | if safeToDelete {
56 | // remove the package from the manifest
57 | var updatedManifest = manifest
58 | updatedManifest.remove(package: package)
59 |
60 | // run the remove action for any packages removed
61 | engine.processUpdate(from: manifest, to: updatedManifest)
62 |
63 | // try to write the updated manifest
64 | let resolved = try engine.updateManifest(from: manifest, to: updatedManifest)
65 |
66 | // check that the count went down
67 | guard resolved.dependencies.count < manifest.dependencies.count else {
68 | output.log("Couldn't remove `\(package.name)`.")
69 | return
70 | }
71 |
72 | output.log("Package \(package.name) removed.")
73 | }
74 |
75 | //
76 | // // check the git status
77 | // if package.installed {
78 | // } else {
79 | // // local directory seems to be missing - also safe to delete in that case
80 | // safeToDelete = true
81 | // }
82 | //
83 | // if safeToDelete {
84 |
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Commands/RenameCommand.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 02/08/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import ArgumentParser
8 |
9 | public struct RenameCommand: ParsableCommand {
10 |
11 | static public var configuration: CommandConfiguration = CommandConfiguration(
12 | commandName: "rename",
13 | abstract: "Rename a package."
14 | )
15 |
16 | public init() {
17 | }
18 |
19 | public func run() throws {
20 | // let manifest = engine.loadManifest()
21 | // let package = engine.existingPackage(manifest: manifest)
22 | // let oldName = package.name
23 | // if package.installed {
24 | // engine.attempt(action: "Rename") {
25 | // let name = try engine.arguments.expectedArgument("name")
26 | // try package.rename(as: name, engine: engine)
27 | // engine.output.log("Renamed \(oldName) as \(name).")
28 | // }
29 | // }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Commands/RepairCommand.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 14/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import ArgumentParser
8 | import Runner
9 | import CommandShell
10 |
11 | public struct RepairCommand: ParsableCommand {
12 | @OptionGroup() var common: CommandShellOptions
13 |
14 | static public var configuration: CommandConfiguration = CommandConfiguration(
15 | commandName: "repair",
16 | abstract: "Attempt to repair a corrupt manifest."
17 | )
18 |
19 | public init() {
20 | }
21 |
22 | public func run() throws {
23 | let engine: Engine = common.loadEngine()
24 | try engine.repair()
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Commands/RevealCommand.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 13/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import ArgumentParser
8 | import CommandShell
9 |
10 | public struct RevealCommand: ParsableCommand {
11 | @Argument(help: "The package to reveal.") var packageName: String
12 | @Flag(help: "Print the package path.") var path = false
13 | @OptionGroup() var common: CommandShellOptions
14 |
15 | static public var configuration: CommandConfiguration = CommandConfiguration(
16 | commandName: "reveal",
17 | abstract: "Reveal a package in the finder."
18 | )
19 |
20 | public init() {
21 | }
22 |
23 | public func run() throws {
24 | let engine: Engine = common.loadEngine()
25 | let manifest = try engine.loadManifest()
26 | let package = try engine.existingPackage(from: packageName, manifest: manifest)
27 | if path {
28 | engine.output.log(package.path)
29 | } else {
30 | package.reveal()
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Commands/UpdateCommand.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 14/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import ArgumentParser
8 | import Runner
9 | import CommandShell
10 |
11 | public struct UpdateCommand: ParsableCommand {
12 | @Flag(name: .customLong("self"), help: "Update xpkg itself.") var updateSelf = false
13 | @Argument(help: "The package to update.") var packageName: String?
14 | @OptionGroup() var common: CommandShellOptions
15 |
16 | static public var configuration: CommandConfiguration = CommandConfiguration(
17 | commandName: "update",
18 | abstract: "Update a package to the latest version."
19 | )
20 |
21 | public init() {
22 | }
23 |
24 | public func run() throws {
25 | let engine: Engine = common.loadEngine()
26 | if updateSelf {
27 | try engine.update()
28 | } else if let packageName = packageName {
29 | let manifest = try engine.loadManifest()
30 | let package = try engine.existingPackage(from: packageName, manifest: manifest)
31 | update(package: package, engine: engine)
32 | } else {
33 | engine.output.log("Checking all packages for updates...")
34 | let _ = try engine.forEachPackage { (package) in
35 | update(package: package, engine: engine)
36 | }
37 | engine.output.log("Done.")
38 | }
39 | }
40 |
41 | func update(package: Package, engine: Engine) {
42 | let state = package.needsUpdate(engine: engine)
43 | switch state {
44 | case .needsUpdate(let newerVersion):
45 | package.update(to: newerVersion, engine: engine)
46 |
47 | case .unchanged:
48 | engine.output.log("Package \(package.name) was unchanged.")
49 |
50 | case .notOnTag:
51 | engine.output.log("Package \(package.name) is modified locally or not at a published version.")
52 |
53 | default:
54 | engine.output.log("Could not fetch the latest version for \(package.name), so it has not been updated.")
55 | }
56 | }
57 |
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Engine.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 08/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import ArgumentParser
8 | import CommandShell
9 | import Expressions
10 | import Files
11 | import Foundation
12 | import Logger
13 | import Runner
14 | import SemanticVersion
15 | import XPkgPackage
16 |
17 | #if canImport(FoundationNetworking)
18 | import FoundationNetworking
19 | #endif
20 |
21 | extension URLSession {
22 | func synchronousDataTask(with request: URLRequest) -> (Data?, URLResponse?, Error?) {
23 | var data: Data?
24 | var response: URLResponse?
25 | var error: Error?
26 |
27 | let semaphore = DispatchSemaphore(value: 0)
28 |
29 | let dataTask = self.dataTask(with: request) {
30 | data = $0
31 | response = $1
32 | error = $2
33 |
34 | semaphore.signal()
35 | }
36 | dataTask.resume()
37 |
38 | _ = semaphore.wait(timeout: .distantFuture)
39 |
40 | return (data, response, error)
41 | }
42 | }
43 |
44 | public class Engine: CommandEngine {
45 | static let missingProductPattern = try! NSRegularExpression(pattern: #"'vault': product '(.*)' required by package 'vault' target 'Installed' not found in package '(.*)'."#)
46 | // 'vault': product 'Logger-xpkg-hooks' required by package 'vault' target 'Installed' not found in package 'Logger'
47 | let jsonChannel: Channel
48 | let fileManager = FileManager.default
49 |
50 | var defaultOrgs = ["elegantchaos", "samdeane"] // TODO: read from preference
51 |
52 | public required init(options: CommandShellOptions, info: [String:Any]?) {
53 | jsonChannel = Channel("json", handlers: [Channel.stdoutHandler])
54 | super.init(options: options, info: info)
55 | }
56 |
57 | public override class var abstract: String {
58 | return "Cross Platform Package Manager."
59 | }
60 |
61 | public override class var subcommands: [ParsableCommand.Type] {
62 | return [
63 | InitCommand.self,
64 | CheckCommand.self,
65 | InstallCommand.self,
66 | LinkCommand.self,
67 | ListCommand.self,
68 | PathCommand.self,
69 | RebuildCommand.self,
70 | ReinstallCommand.self,
71 | RemoveCommand.self,
72 | RenameCommand.self,
73 | RepairCommand.self,
74 | RevealCommand.self,
75 | UpdateCommand.self,
76 | ]
77 | }
78 |
79 |
80 | internal var xpkgURL: URL {
81 | let localPath = ("~/.local/share/xpkg" as NSString).expandingTildeInPath as String
82 | let localURL = URL(fileURLWithPath: localPath).resolvingSymlinksInPath()
83 |
84 | if fileManager.fileExists(at: localURL) {
85 | return localURL
86 | } else {
87 | let globalURL = URL(fileURLWithPath: "/usr/local/share/xpkg").resolvingSymlinksInPath()
88 | if fileManager.fileExists(atPath: globalURL.path) {
89 | return globalURL
90 | }
91 | }
92 |
93 | try? fileManager.createDirectory(at: localURL, withIntermediateDirectories: true)
94 | return localURL
95 | }
96 |
97 | internal var xpkgCodeURL: URL {
98 | return xpkgURL.appendingPathComponent("code")
99 | }
100 |
101 | /// Returns the latest semantic version tag for the remote repo.
102 | /// If the tag contains a "v", we return it along with the number.
103 | /// - Parameter url: the url of the repo
104 | internal func latestVersion(_ url: URL) -> String? {
105 | let runner = Runner(for: gitURL)
106 | let arguments = ["ls-remote", "--tags", "--refs", "--sort=v:refname", "--exit-code", url.absoluteString ]
107 |
108 | var error = ""
109 | let callback = Runner.Mode.callback({ text in
110 | error.append(text)
111 | // TODO: filter out some known errors and exit or prompt for the user to fix them?
112 | })
113 |
114 | // TODO: add a timeout?
115 | guard let result = try? runner.sync(arguments: arguments, stdoutMode: .capture, stderrMode: callback), result.status == 0 else {
116 | return nil
117 | }
118 |
119 | guard let tag = result.stdout.split(separator: "/").last else {
120 | return ""
121 | }
122 |
123 | guard tag.contains(".") else {
124 | return ""
125 | }
126 |
127 | return String(tag.trimmingCharacters(in: .whitespacesAndNewlines))
128 | }
129 |
130 | typealias RepoValidator = (URL) -> String?
131 |
132 | func validate(_ remote: URL) -> String? {
133 | let version = latestVersion(remote)
134 | return version?.replacingOccurrences(of: "v", with: "")
135 | }
136 |
137 | internal var gitLooksLikeItIsSetup: Bool {
138 | let hosts = FileManager.default.locations.home.folder(".ssh").file("known_hosts")
139 | return hosts.asText?.contains("github.com") ?? false
140 | }
141 |
142 | /// Given a package spec, try to find a URL and latest version for the package.
143 | /// The spec can be one of:
144 | /// - a full URL - which is used directly
145 | /// - an unqualified package name - we try to make a github URL using one of the default organisations
146 | /// - a qualified org/package page - we try to make this into a github URL
147 | /// - Parameter package: the package spec
148 | /// - Parameter skipValidation: whether to perform online validation; provided for testing
149 | internal func remotePackageURL(_ package: String, validator: RepoValidator? = nil) throws -> (URL, String?) {
150 | let validate = validator ?? { url in self.validate(url) }
151 |
152 | if let remote = URL(string: package), let version = validate(remote) {
153 | return (remote, version)
154 | }
155 |
156 | let tryPrefixing = !package.starts(with: "xpkg-")
157 | let containsSlash = package.contains("/")
158 |
159 | var paths = ["\(package)"]
160 | if !containsSlash {
161 | for org in defaultOrgs {
162 | paths.append("\(org)/\(package)")
163 | if tryPrefixing {
164 | paths.append("\(org)/xpkg-\(package)")
165 | }
166 | }
167 | }
168 |
169 | var methods = ["https://github.com/"]
170 | if gitLooksLikeItIsSetup {
171 | // try git@ first if it looks like github is in the known hosts etc
172 | methods.insert("git@github.com:", at: 0)
173 | }
174 |
175 | for method in methods {
176 | for path in paths {
177 | verbose.log("Trying \(method)\(path)")
178 | if let remote = URL(string: "\(method)\(path)"), let version = validate(remote) {
179 | return (remote, version)
180 | }
181 | }
182 | }
183 |
184 | throw Failure.badPackageSpec(package)
185 | }
186 |
187 | internal var vaultURL: URL {
188 | let url = xpkgURL.appendingPathComponent("vault")
189 |
190 | try? fileManager.createDirectory(at: url, withIntermediateDirectories: true)
191 | return url
192 | }
193 |
194 | internal var gitURL: URL {
195 | return URL(fileURLWithPath: "/usr/bin/git")
196 | }
197 |
198 | internal var swiftURL: URL {
199 | return URL(fileURLWithPath: "/usr/bin/swift")
200 | }
201 |
202 | internal var projectsURL: URL {
203 | let paths = ["~/Projects", "~/Developer/Projects"]
204 | for path in paths {
205 | let url = URL(fileURLWithPath: (path as NSString).expandingTildeInPath)
206 | if FileManager.default.fileExists(at: url) {
207 | return url
208 | }
209 | }
210 | return URL(fileURLWithPath: "Projects")
211 | }
212 |
213 | func swift(_ arguments: [String], failureMessage: @autoclosure () -> String = "") -> Runner.Result? {
214 | let runner = Runner(for: swiftURL, cwd: vaultURL)
215 | do {
216 | let result = try runner.sync(arguments: arguments)
217 | if result.status != 0 {
218 | let message = failureMessage()
219 | if !message.isEmpty { output.log(message) }
220 | verbose.log(result.stderr)
221 | }
222 | return result
223 | } catch {
224 | let message = failureMessage()
225 | if !message.isEmpty { output.log(message) }
226 | verbose.log(error)
227 | return nil
228 | }
229 | }
230 |
231 | func attempt(action: String, cleanup: (() throws -> Void)? = nil, block: () throws -> ()) {
232 | verbose.log(action)
233 | do {
234 | try block()
235 | } catch {
236 | try? cleanup?()
237 | output.log("\(action) failed.\n\(error)")
238 | }
239 | }
240 |
241 | var dependencyCacheURL: URL {
242 | vaultURL.appendingPathComponent("Dependencies.json")
243 | }
244 |
245 | var manifestURL: URL {
246 | vaultURL.appendingPathComponent("Package.swift")
247 | }
248 |
249 | func dependencyData(readCache: Bool, writeCache: Bool) throws -> Data {
250 | let cachedURL = dependencyCacheURL
251 |
252 | if readCache, let cached = try? Data(contentsOf: cachedURL) {
253 | verbose.log("Loaded cached dependencies.")
254 | return cached
255 | }
256 |
257 | verbose.log("Reading dependencies from \(manifestURL).")
258 | guard let showResult = swift(["package", "show-dependencies", "--format", "json"]) else {
259 | throw Failure.couldntLoadDependencyData
260 | }
261 |
262 | let code = showResult.status
263 | let output = showResult.stderr
264 | guard code == 0 else {
265 | let range = NSRange(location: 0, length: output.count)
266 | let matches = Self.missingProductPattern.matches(in: output, range: range)
267 | if let match = matches.first {
268 | let product = (output as NSString).substring(with: match.range(at: 1))
269 | let package = (output as NSString).substring(with: match.range(at: 2))
270 | throw Failure.packageMissingHooks(product, package)
271 | }
272 |
273 | throw Failure.errorLoadingDependencyData(code, output)
274 | }
275 |
276 | var json = showResult.stdout
277 | if let index = json.firstIndex(of: "{") {
278 | json.removeSubrange(json.startIndex ..< index)
279 | }
280 |
281 | jsonChannel.log(json)
282 |
283 | if writeCache {
284 | verbose.log("Saved cached dependencies.")
285 | try? json.write(to: cachedURL, atomically: true, encoding: .utf8)
286 | }
287 |
288 | return json.data(using: .utf8)!
289 | }
290 |
291 | func emptyManifest() -> Package {
292 | Package(name: "XPkgVault")
293 | }
294 |
295 | func loadManifest(readCache: Bool = true, writeCache: Bool = true, createIfMissing: Bool = false) throws -> Package {
296 |
297 | if !fileManager.fileExists(atURL: manifestURL), createIfMissing {
298 | return emptyManifest()
299 | }
300 |
301 | let data = try dependencyData(readCache: readCache, writeCache: writeCache)
302 |
303 | do {
304 | let decode = JSONDecoder()
305 | let manifestPackage = try decode.decode(Package.self, from: data)
306 | return manifestPackage
307 | } catch {
308 | verbose.log("Failed to decode manifest.")
309 | verbose.log(error)
310 | throw Failure.couldntDecodeDependencyData
311 | }
312 | }
313 |
314 | func saveManifestAndRemoveCachedDependencies(manifest: Package) {
315 | var dependencies = ""
316 | var products = ""
317 |
318 | for package in manifest.dependencies {
319 | let version = package.version
320 | if version.isEmpty || version == "unspecified" {
321 | dependencies.append(" .package(url: \"\(package.url)\", Version(0,0,1)...Version(10000,0,0)),\n")
322 | } else {
323 | dependencies.append(" .package(url: \"\(package.url)\", from:\"\(version)\"),\n")
324 | }
325 | products.append(" .product(name: \"\(package.name)-xpkg-hooks\", package: \"\(package.name)\"),\n")
326 | }
327 |
328 | let manifestText = """
329 | // swift-tools-version:5.2
330 |
331 | import PackageDescription
332 | let package = Package(
333 | name: "XPkgVault",
334 | platforms: [
335 | .macOS(.v10_15)
336 | ],
337 | products: [
338 | ],
339 | dependencies: [
340 | \(dependencies)
341 | ],
342 | targets: [
343 | .target(
344 | name: "Installed",
345 | dependencies: [
346 | \(products)
347 | ]
348 | ),
349 | ]
350 | )
351 | """
352 |
353 | let url = vaultURL.appendingPathComponent("Package.swift")
354 | do {
355 | try createInstalledSourceStub()
356 | try manifestText.write(to: url, atomically: true, encoding: .utf8)
357 | removeDependencyCache()
358 | } catch {
359 | verbose.log(error)
360 | }
361 | }
362 |
363 | func createInstalledSourceStub() throws {
364 | let sourcesURL = vaultURL.appendingPathComponent("Sources").appendingPathComponent("Installed")
365 | try? fileManager.createDirectory(at: sourcesURL, withIntermediateDirectories: true, attributes: nil)
366 | let mainURL = sourcesURL.appendingPathComponent("main.swift")
367 | try "".write(to: mainURL, atomically: true, encoding: .utf8)
368 | }
369 |
370 | func removeDependencyCache() {
371 | try? fileManager.removeItem(at: dependencyCacheURL)
372 | }
373 |
374 | func updateManifest(from oldPackage: Package, to newPackage: Package) throws -> Package {
375 | let backupURL = manifestURL.appendingPathExtension("backup")
376 | do {
377 | try fileManager.moveItem(at: manifestURL, to: backupURL)
378 | verbose.log("Backed up manifest to \(backupURL).")
379 | } catch {
380 | verbose.log("Failed to backup existing manifest.")
381 | }
382 |
383 | defer {
384 | verbose.log("Removing backup if still present.")
385 | try? fileManager.removeItem(at: backupURL)
386 | }
387 |
388 | saveManifestAndRemoveCachedDependencies(manifest: newPackage)
389 |
390 | do {
391 | let resolved = try loadManifest()
392 | return resolved
393 | } catch {
394 | // backup failed manifest for debugging
395 | let date = DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .short)
396 | let failedURL = vaultURL.appendingPathComponent("Failed Package \(date).swift")
397 | try? fileManager.moveItem(at: manifestURL, to: failedURL)
398 | verbose.log("Failed to load updated manifest. Saved manifest as \(failedURL) for inspection.")
399 |
400 | do {
401 | try fileManager.moveItem(at: backupURL, to: manifestURL)
402 | verbose.log("Restored manifest from backup.")
403 | } catch {
404 | verbose.log("Failed to restore previous manifest. Will attempt to recreate it.")
405 | saveManifestAndRemoveCachedDependencies(manifest: oldPackage)
406 | }
407 |
408 | throw error
409 | }
410 | }
411 |
412 | func processUpdate(from: Package, to: Package) {
413 | let (_, before) = from.allPackages
414 | let (after, _) = to.allPackages
415 |
416 | let beforeSet = Set(before)
417 | for package in after {
418 | if !beforeSet.contains(package) {
419 | do {
420 | if try package.run(action: "install", engine: self) {
421 | output.log("Added \(package.name).")
422 | }
423 | } catch {
424 | output.log("Install action for \(package.name) failed.")
425 | }
426 | }
427 | }
428 |
429 | let afterSet = Set(after)
430 | for package in before {
431 | if !afterSet.contains(package) {
432 | do {
433 | if try package.run(action:"remove", engine: self) {
434 | output.log("Removed \(package.name).")
435 | }
436 | } catch {
437 | output.log("Remove action for \(package.name) failed.")
438 | }
439 | }
440 | }
441 | }
442 |
443 | func forEachPackage(_ block: (Package) -> ()) throws -> Bool {
444 | let manifest = try loadManifest()
445 |
446 | if manifest.dependencies.count == 0 {
447 | return false
448 | }
449 | for package in manifest.dependencies {
450 | block(package)
451 | }
452 | return true
453 | }
454 |
455 | /**
456 | Return a package structure for an existing package, if it exists
457 | */
458 |
459 | func possiblePackage(named name: String, manifest: Package) -> Package? {
460 | return manifest.package(named: name)
461 | }
462 |
463 | /**
464 | Return a package structure for an existing package that was specified as
465 | an argument.
466 | */
467 |
468 | func existingPackage(from packageName: String, manifest: Package) throws -> Package {
469 | guard let package = possiblePackage(named: packageName, manifest: manifest) else {
470 | throw Failure.packageNotInstalled(packageName)
471 | }
472 |
473 | return package
474 | }
475 |
476 | // func fail(_ failure: Failure) -> Never {
477 | // let message: String
478 | // let code: Int32
479 | //
480 | // switch failure {
481 | // case .packageNotInstalled(let packageName):
482 | // message = "Package \(packageName) is not installed."
483 | // code = 1
484 | //
485 | // case .couldntLoadManifest:
486 | // message = "Manifest is missing or corrupt."
487 | // code = 2
488 | // }
489 | //
490 | // output.log(message)
491 | // exit(code)
492 | // }
493 |
494 | func exit(_ code: Int32) -> Never {
495 | Manager.shared.flush()
496 | Foundation.exit(code)
497 | }
498 |
499 | func remoteOriginForCwd() -> String {
500 | return remoteOrigin() ?? ""
501 | }
502 |
503 | func remoteOrigin(forLocalURL url: URL? = nil) -> String? {
504 | let runner = Runner(for: gitURL, cwd: url)
505 | guard let result = try? runner.sync(arguments: ["remote", "get-url", "origin"]), result.status == 0 else { return nil }
506 | return result.stdout.trimmingCharacters(in: CharacterSet.newlines)
507 | }
508 |
509 | func remoteURL(forLocalURL url: URL?) -> URL? {
510 | guard let origin = remoteOrigin(forLocalURL: url) else { return nil }
511 | return URL(string: origin)
512 |
513 | }
514 |
515 | func localRepoForCwd()-> String {
516 | let runner = Runner(for: gitURL)
517 | if let result = try? runner.sync(arguments: ["rev-parse", "--show-toplevel"]) {
518 | if result.status == 0 {
519 | return result.stdout.trimmingCharacters(in: CharacterSet.newlines)
520 | }
521 | }
522 | return ""
523 | }
524 |
525 | func gitUserName() -> String? {
526 | let runner = Runner(for: gitURL)
527 | if let result = try? runner.sync(arguments: ["config", "--global", "user.name"]) {
528 | if result.status == 0 {
529 | return result.stdout.trimmingCharacters(in: CharacterSet.newlines)
530 | }
531 | }
532 | return nil
533 | }
534 |
535 | /// Pull the latest version of XPkg, then re-run the bootstrap script
536 | /// to build & install it.
537 | func update() throws {
538 | output.log("Updating xpkg.")
539 | let url = xpkgURL
540 | let codeURL = url.appendingPathComponent("code")
541 | let runner = Runner(for: gitURL, cwd: codeURL)
542 | let result = try runner.sync(arguments: ["pull"])
543 | output.log(result.stdout)
544 |
545 | try runBootstrap()
546 | }
547 |
548 | /// Run the bootstrap script.
549 | /// This will rebuild Xpkg and (re)install any essential links.
550 | func runBootstrap() throws {
551 | output.log("Rebuilding.")
552 | let codeURL = xpkgURL.appendingPathComponent("code")
553 | let bootstrapURL = codeURL.appendingPathComponent(".bin").appendingPathComponent("bootstrap")
554 | let bootstrapRunner = Runner(for: bootstrapURL, cwd: codeURL)
555 | let result = try bootstrapRunner.sync()
556 | output.log(result.stdout)
557 | }
558 |
559 | /// Install a package.
560 | func install(packageSpec: String, asProject: Bool = false, asName: String? = nil, linkTo: URL? = nil) throws {
561 | // load the existing manifest; if it's missing we'll create a new empty one
562 | let manifest = try loadManifest(createIfMissing: true)
563 |
564 | // do a quick check first for an existing local package with the name/spec
565 | if let existingPackage = manifest.package(matching: packageSpec) {
566 | output.log("Package `\(existingPackage.name)` is already installed.")
567 | return
568 | }
569 |
570 | // resolve the spec to a full url and a version
571 | output.log("Searching for package \(packageSpec)...")
572 | let (url, version) = try remotePackageURL(packageSpec)
573 |
574 | // now we have a full spec, check again to see if it's already installed
575 | if let existingPackage = manifest.package(matching: url.deletingPathExtension().path) {
576 | output.log("Package `\(existingPackage.name)` is already installed. Use `\(name) upgrade \(packageSpec)` to upgrade it to the latest version.")
577 | return
578 | }
579 |
580 | if let version = version, !version.isEmpty {
581 | output.log("Installing \(url.path) \(version).")
582 | } else {
583 | output.log("Installing \(url.path).")
584 | }
585 |
586 | // add the package to the manifest
587 | verbose.log("Adding package to manifest.")
588 | let newPackage = Package(url: url, version: version ?? "")
589 | var updatedManifest = manifest
590 | updatedManifest.add(package: newPackage)
591 |
592 | // try to write the update
593 | verbose.log("Writing manifest.")
594 | let resolved = try updateManifest(from: manifest, to: updatedManifest)
595 |
596 | guard resolved.dependencies.count > manifest.dependencies.count else {
597 | output.log("Couldn't add `\(packageSpec)`.")
598 | return
599 | }
600 |
601 | // link into project if requested
602 | guard let installedPackage = resolved.package(withURL: url) else {
603 | output.log("Couldn't find package.")
604 | verbose.log(resolved.dependencies.map({ $0.url }))
605 | return
606 | }
607 |
608 | let specifyLink = asProject || (linkTo != nil)
609 | let name = asName ?? installedPackage.name
610 | var linkURL: URL? = nil
611 | if specifyLink {
612 | let pathURL = linkTo ?? projectsURL.appendingPathComponent(name)
613 | verbose.log("Linking package into \(pathURL.path).")
614 | linkURL = pathURL
615 | } else {
616 | verbose.log("Linking package into Packages/.")
617 | }
618 |
619 | installedPackage.edit(at: linkURL, engine: self)
620 |
621 | // if it wrote ok, run the install actions for any new packages
622 | // we need to reload the package once again as it has moved
623 | let reloadedManifest = try loadManifest(readCache: false, writeCache: true)
624 |
625 | verbose.log("Running actions for new packages.")
626 | processUpdate(from: manifest, to: reloadedManifest)
627 | }
628 |
629 | /// Returns the version of the package, according to `git describe`
630 | /// - Parameter engine: The current engine.
631 | func currentVersion(forLocalURL url: URL) -> String? {
632 | let runner = Runner(for: gitURL, cwd: url)
633 | guard let result = try? runner.sync(arguments: ["describe", "--tags"]), result.status == 0 else { return nil }
634 | let fullVersion = result.stdout.trimmingCharacters(in: .whitespacesAndNewlines)
635 | let version = SemanticVersion(fullVersion)
636 | return version.isInvalid ? nil : version.asString(dropTrailingZeros: false)
637 | }
638 |
639 | /// Given a local directory, try to make a Package.
640 | /// This will work if it's a git directory with an origin remote set,
641 | /// and a tagged version.
642 | func reconstructPackage(from localURL: URL) -> Package? {
643 | guard localURL.isFileURL else { return nil }
644 |
645 | guard let remoteURL = remoteURL(forLocalURL: localURL) else { return nil }
646 | let version = currentVersion(forLocalURL: localURL) ?? "unspecified"
647 |
648 | return Package(localURL: localURL, remoteURL: remoteURL, version: version)
649 | }
650 |
651 | /// Attempt to repair a corrupt manifest.
652 | /// We look in the Packages/ folder inside the value. Each folder in there should be a package,
653 | /// so we make a new empty manifest, and then try to install each package in turn.
654 | func repair() throws {
655 | let packages = fileManager.locations.folder(for: vaultURL).folder("Packages")
656 | if packages.exists {
657 | var manifest = emptyManifest()
658 | try packages.forEach(filter: .folders, recursive: false) { folder in
659 | if let package = reconstructPackage(from: folder.url) {
660 | manifest.add(package: package)
661 | }
662 | }
663 | saveManifestAndRemoveCachedDependencies(manifest: manifest)
664 | }
665 | }
666 | }
667 |
668 | extension SemanticVersion {
669 | public func asString(dropTrailingZeros: Bool = true) -> String {
670 | guard !isInvalid else {
671 | return ""
672 | }
673 |
674 | guard !isUnknown else {
675 | return ""
676 | }
677 |
678 | var string = "\(major).\(minor)"
679 | if !dropTrailingZeros || patch != 0 {
680 | string += ".\(patch)"
681 | }
682 | return string
683 | }
684 |
685 | }
686 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Failure.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane on 03/08/22.
3 | // All code (c) 2022 - present day, Elegant Chaos Limited.
4 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
5 |
6 | import Foundation
7 |
8 | enum Failure: Error {
9 | case packageNotInstalled(String)
10 | case couldntLoadDependencyData
11 | case errorLoadingDependencyData(Int32, String)
12 | case couldntDecodeDependencyData
13 | case badPackageSpec(String)
14 | case packageMissingHooks(String, String)
15 | }
16 |
17 | extension Failure: CustomStringConvertible {
18 | var description: String {
19 | switch self {
20 | case .packageNotInstalled(let name):
21 | return "Package “\(name)” is not installed."
22 |
23 | case .packageMissingHooks(let product, let package):
24 | return "Package “\(package)” doesn't have an \(product) product. It is probably not an XPkg package."
25 |
26 | case .errorLoadingDependencyData(let code, let error):
27 | return "Swift returned error \(code) whilst fetching dependency data: \(error)"
28 | case .couldntLoadDependencyData:
29 | return "The dependency data is missing."
30 |
31 | case .couldntDecodeDependencyData:
32 | return "The depedency data is corrupt."
33 |
34 | case .badPackageSpec(let spec):
35 | return "Can't find package “\(spec)”. Packages should be in the form “user/package” or “oganisation/package”."
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/Package.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 12/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import Foundation
8 | import Logger
9 | import Runner
10 | import XPkgPackage
11 | import SemanticVersion
12 |
13 | #if canImport(AppKit)
14 | import AppKit
15 | #endif
16 |
17 | enum RenameError: Error {
18 | case renameStore(from: URL, to: URL)
19 | case renameLocal
20 | case saveInfo
21 | }
22 |
23 | struct PackageInfo: Codable {
24 | let version: Int
25 | let name: String
26 | let remote: URL
27 | let local: URL
28 | let linked: Bool
29 | let removeable: Bool
30 |
31 | init(version: Int = 1, name: String, remote: URL, local: URL, linked: Bool, removeable: Bool) {
32 | self.version = version
33 | self.name = name
34 | self.remote = remote
35 | self.local = local
36 | self.linked = linked
37 | self.removeable = removeable
38 | }
39 | }
40 |
41 | enum PackageError: Error, CustomStringConvertible {
42 | case failedToClone(repo: String)
43 |
44 | var description: String {
45 | switch self {
46 | case .failedToClone(let repo):
47 | return "Couldn't clone from \(repo)."
48 | }
49 | }
50 | }
51 |
52 | enum PackageStatus {
53 | case pristine
54 | case modified
55 | case ahead
56 | case unknown
57 | case untracked
58 | case uncommitted
59 | }
60 |
61 | struct Package: Decodable {
62 | let name: String
63 | let version: String
64 | let path: String
65 | let url: String
66 | var dependencies: [Package]
67 |
68 | init(url: URL, version: String) {
69 | self.name = url.lastPathComponent
70 | self.path = ""
71 | self.url = url.absoluteString
72 | self.version = version
73 | self.dependencies = []
74 | }
75 |
76 | init(name: String) {
77 | self.name = name
78 | self.path = ""
79 | self.url = ""
80 | self.version = ""
81 | self.dependencies = []
82 | }
83 |
84 | init(localURL: URL, remoteURL: URL, version: String) {
85 | self.name = localURL.deletingPathExtension().lastPathComponent
86 | self.path = localURL.path
87 | self.url = remoteURL.absoluteString
88 | self.version = version
89 | self.dependencies = []
90 | }
91 |
92 | var global: Bool { return false }
93 | var local: URL { return URL(fileURLWithPath: path) }
94 |
95 | lazy var remote: URL = {
96 | return URL(string: url)!
97 | }()
98 |
99 | lazy var normalized: URL = {
100 | remote.normalizedGitURL
101 | }()
102 |
103 | lazy var qualified: String = {
104 | let components = normalized.pathComponents
105 | return components[1...].joined(separator: "/")
106 | }()
107 |
108 | func package(matching spec: String) -> Package? {
109 | let url = URL(string:spec)
110 | let normalised = url == nil ? spec : url!.normalizedGitURL.absoluteString
111 | for var package in dependencies {
112 | if package.name == spec {
113 | return package
114 | } else if package.qualified == spec {
115 | return package
116 | } else if package.normalized.absoluteString == normalised {
117 | return package
118 | }
119 | }
120 | return nil
121 | }
122 |
123 | func name(contains string: String) -> Bool {
124 | let prefix = "git@github.com:"
125 | if url.starts(with: prefix) {
126 | let start = url.index(url.startIndex, offsetBy: prefix.count)
127 | let path = url[start...]
128 | return path.contains(string)
129 | }
130 |
131 | if let url = URL(string: url) {
132 | return url.path.contains(string)
133 | }
134 |
135 | return false
136 | }
137 |
138 | func package(named name: String) -> Package? {
139 | for package in dependencies {
140 | if (package.name == name) || (package.name == "xpkg-\(name)") || package.name(contains: name) {
141 | return package
142 | }
143 | }
144 | return nil
145 | }
146 |
147 | func package(withURL url: URL) -> Package? {
148 | let normalised = url.normalizedGitURL
149 | for var package in dependencies {
150 | if package.normalized == normalised {
151 | return package
152 | }
153 | }
154 | return nil
155 | }
156 |
157 | class DepthsIndex {
158 | var index: [Package:Depths] = [:]
159 |
160 | func record(package: Package, depth: Int) {
161 | if let depths = index[package] {
162 | depths.record(depth: depth)
163 | } else {
164 | index[package] = Depths(depth: depth)
165 | }
166 | }
167 | }
168 |
169 | class Depths {
170 | var highest: Int
171 | var lowest: Int
172 |
173 | init(depth: Int) {
174 | highest = depth
175 | lowest = depth
176 | }
177 |
178 | func record(depth: Int) {
179 | highest = max(depth, highest)
180 | lowest = min(depth, lowest)
181 | }
182 | }
183 |
184 | func recordPackages(index: DepthsIndex, depth: Int) {
185 | for package in dependencies {
186 | index.record(package: package, depth: depth)
187 | package.recordPackages(index: index, depth: depth + 1)
188 | }
189 | }
190 |
191 | var allPackages: ([Package], [Package]) {
192 | let index = DepthsIndex()
193 | recordPackages(index: index, depth: 1)
194 |
195 | let byMostDependent = index.index.sorted { (p1, p2) -> Bool in
196 | return p1.value.highest > p2.value.highest
197 | }
198 |
199 | let byLeastDependent = index.index.sorted { (p1, p2) -> Bool in
200 | return p1.value.lowest < p2.value.lowest
201 | }
202 |
203 | return (byMostDependent.map({ $0.key }), byLeastDependent.map({ $0.key }))
204 | }
205 |
206 | mutating func add(package: Package) {
207 | dependencies.append(package)
208 | }
209 |
210 | mutating func remove(package: Package) {
211 | if let index = dependencies.firstIndex(where: { $0.name == package.name }) {
212 | dependencies.remove(at: index)
213 | }
214 | }
215 |
216 |
217 | /**
218 | Return the default location to use for the local (hidden) clone of a package.
219 | */
220 |
221 | static func defaultLocalURL(for name: String, in store: URL) -> URL {
222 | return store.appendingPathComponent("local").appendingPathComponent(name)
223 | }
224 |
225 | /**
226 | Link package to an existing folder.
227 | */
228 |
229 | func edit(at url: URL? = nil, engine: Engine) {
230 | var args = ["package", "edit", name]
231 | let message: String
232 | if let url = url {
233 | args.append(contentsOf: ["--path", url.path])
234 | message = "Failed to link \(name) into \(url)."
235 | } else {
236 | message = "Failed to edit \(name)."
237 | }
238 |
239 | engine.removeDependencyCache()
240 | let _ = engine.swift(args, failureMessage: message)
241 | }
242 |
243 | /**
244 | Link package to an existing folder.
245 | */
246 |
247 | func unedit(engine: Engine) -> Bool {
248 | engine.removeDependencyCache()
249 | guard let result = engine.swift(["package", "unedit", name]) else {
250 | engine.output.log("Failed to unlink \(name) from \(path).")
251 | return false
252 | }
253 |
254 | return result.status == 0 || result.stderr.contains("not in edit mode")
255 | }
256 |
257 |
258 | /**
259 | Reveal the package in the Finder/Desktop.
260 | */
261 |
262 | func reveal() {
263 | #if canImport(AppKit)
264 | NSWorkspace.shared.open([local], withAppBundleIdentifier: nil, options: .async, additionalEventParamDescriptor: nil, launchIdentifiers: nil)
265 | #else
266 | let runner = Runner(for: URL(fileURLWithPath: "/usr/bin/env"), cwd: local)
267 | let _ = try? runner.sync(arguments: ["xdg-open", "."])
268 | #endif
269 | }
270 |
271 | /**
272 | What state is the local package in?
273 | */
274 |
275 | func status(engine: Engine) -> PackageStatus {
276 | let runner = Runner(for: engine.gitURL, cwd: local)
277 | if let result = try? runner.sync(arguments: ["status", "--porcelain", "--branch"]) {
278 | engine.verbose.log(result.stdout)
279 | if result.status == 0 {
280 | let lines = result.stdout.split(separator: "\n")
281 | if lines.count > 0 {
282 | let branch = lines[0]
283 | if branch == "## No commits yet on master" {
284 | return .uncommitted
285 | } else {
286 | let output = lines.dropFirst().joined(separator: "\n")
287 | if output == "" {
288 | let branchOk = branch.contains("...") || branch == "## HEAD (no branch)"
289 | return branchOk ? .pristine : .untracked
290 | } else if output.contains("??") || output.contains(" D ") || output.contains(" M ") || output.contains("R ") || output.contains("A ") {
291 | return .modified
292 | } else if output.contains("[ahead ") {
293 | return .ahead
294 | } else {
295 | return .untracked
296 | }
297 | }
298 | }
299 | }
300 | }
301 |
302 | return .unknown
303 | }
304 |
305 |
306 | /// Returns the name of the branch that the package is on, or nil if it isn't on a branch.
307 | /// - Parameter engine: The current engine.
308 | func currentBranch(engine: Engine) -> String? {
309 | let runner = Runner(for: engine.gitURL, cwd: local)
310 | if let result = try? runner.sync(arguments: ["rev-parse", "--abbrev-ref", "HEAD"]) {
311 | engine.verbose.log("current branch: \(result.stdout)")
312 | if result.status == 0 {
313 | let branch = result.stdout.trimmingCharacters(in: .whitespacesAndNewlines)
314 | if branch != "HEAD" {
315 | return branch
316 | }
317 | }
318 | }
319 | return nil
320 | }
321 |
322 | /// Returns the latest version of the package, according to the server.
323 | /// - Parameter engine: the engine
324 | func latestVersion(engine: Engine) -> String? {
325 | return engine.latestVersion(URL(string:url)!)
326 | }
327 |
328 | enum UpdateState {
329 | case unchanged
330 | case needsUpdate(to: String)
331 | case notOnTag
332 | case couldntFetchLatest
333 | case couldntParseLatest
334 | }
335 |
336 | /// Does the package need updating?
337 | /// If it's on a version tag, and there's a newer version available, we return the newer version.
338 | /// If it's on the latest version tag, or checked out to a branch, we return nil.
339 | /// - Parameter engine: The current context.
340 | func needsUpdate(engine: Engine) -> UpdateState {
341 | let current = engine.currentVersion(forLocalURL: local) ?? ""
342 | guard !current.contains("-") else {
343 | let branch = currentBranch(engine: engine)
344 | if branch == nil {
345 | engine.verbose.log("\(name) is a modified version \(current).")
346 | } else {
347 | engine.verbose.log("\(name) is a modified version \(current) on branch \(branch!)")
348 | }
349 | return .notOnTag
350 | }
351 |
352 |
353 | guard let latest = latestVersion(engine: engine) else {
354 | engine.verbose.log("\(name) couldn't fetch latest version.")
355 | return .couldntFetchLatest
356 | }
357 |
358 | let sCurrent = SemanticVersion(current)
359 | let sLatest = SemanticVersion(latest)
360 | guard !sCurrent.isInvalid && !sLatest.isInvalid else {
361 | engine.verbose.log("\(name) couldn't parse versions.")
362 | return .couldntParseLatest
363 | }
364 |
365 | engine.verbose.log("\(name) \(current) \(latest)")
366 | return sCurrent < sLatest ? .needsUpdate(to: latest) : .unchanged
367 | }
368 |
369 | /// Checkout the package to a given branch, tag or other ref.
370 | /// - Parameter ref: the branch, tag, commit or ref to check out to
371 | /// - Parameter engine: the context
372 | func checkout(to ref: String, engine: Engine) {
373 | let runner = Runner(for: engine.gitURL, cwd: local)
374 | guard let result = try? runner.sync(arguments: ["checkout", ref]) else {
375 | engine.output.log("Couldn't checkout package \(name) to \(ref).")
376 | return
377 | }
378 |
379 | engine.verbose.log(result.stdout)
380 | engine.verbose.log(result.stderr)
381 | if result.status != 0 {
382 | if result.stderr.contains("The following untracked working tree files would be overwritten") {
383 | engine.output.log("Package \(name) has untracked files.")
384 | }
385 |
386 | engine.output.log("Couldn't update package \(name) to \(ref).")
387 | }
388 | }
389 |
390 | /// Fetch the latest commits.
391 | /// - Parameter engine: the context
392 | func fetch(engine: Engine) {
393 | let runner = Runner(for: engine.gitURL, cwd: local)
394 | if let result = try? runner.sync(arguments: ["fetch", "--tags"]) {
395 | engine.verbose.log(result.stdout)
396 | engine.verbose.log(result.stderr)
397 | }
398 | }
399 |
400 | /**
401 | Update the package.
402 | */
403 |
404 | func update(to version: String, engine: Engine) {
405 | engine.output.log("Updating \(name) to \(version).")
406 |
407 | engine.verbose.log("Uninstalling \(name).")
408 | if let ok = try? run(action: "remove", engine: engine), ok {
409 | engine.verbose.log("Removed \(name).")
410 | }
411 |
412 | engine.verbose.log("Updating \(name).")
413 | fetch(engine: engine)
414 | checkout(to: version, engine: engine)
415 |
416 | engine.verbose.log("Installing \(name).")
417 | if let ok = try? run(action: "install", engine: engine), ok {
418 | engine.verbose.log("Reinstalled \(name).")
419 | }
420 | }
421 |
422 | /**
423 | Check that the package information seems to be valid.
424 | */
425 |
426 | func check(engine: Engine) -> Bool {
427 | return engine.fileManager.fileExists(at: local)
428 | }
429 |
430 | /**
431 | Rename the package. If it's a project, we also rename the project folder.
432 | */
433 |
434 | func rename(as newName: String, engine: Engine) throws {
435 | // let newStore = store.deletingLastPathComponent().appendingPathComponent(newName)
436 | // do {
437 | // try fileManager.moveItem(at: store, to: newStore)
438 | // self.store = newStore
439 | // } catch {
440 | // throw RenameError.renameStore(from: store, to: newStore)
441 | // }
442 | //
443 | // let oldLocal: URL
444 | // let newLocal: URL
445 | // if linked {
446 | // // package is linked elsewhere, so we just want to rename it
447 | // oldLocal = local
448 | // newLocal = local.deletingLastPathComponent().appendingPathComponent(newName)
449 | //
450 | // } else {
451 | // newLocal = Package.defaultLocalURL(for: newName, in: newStore)
452 | //
453 | // if local.lastPathComponent == name {
454 | // // package is inside the store, which has already been renamed
455 | // // but the local folder itself still needs to be renamed
456 | // oldLocal = Package.defaultLocalURL(for: name, in: newStore)
457 | //
458 | // } else {
459 | // // package is inside store, but was previously just in a folder called "local"
460 | // // we want to fix things up a bit
461 | // let oldStyleLocal = newStore.appendingPathComponent("local")
462 | // oldLocal = newStore.appendingPathComponent("temp-rename")
463 | // try? fileManager.moveItem(at: oldStyleLocal, to: oldLocal)
464 | // try? fileManager.createDirectory(at: oldStyleLocal, withIntermediateDirectories: true)
465 | // }
466 | // }
467 | //
468 | // do {
469 | // try fileManager.moveItem(at: oldLocal, to: newLocal)
470 | // self.local = newLocal
471 | // } catch {
472 | // throw RenameError.renameLocal
473 | // }
474 | //
475 | // self.name = newName
476 | }
477 |
478 | func run(action: String, engine: Engine) throws -> Bool {
479 | do {
480 | // run as a new-style package
481 | let runner = Runner(for: engine.swiftURL, cwd: engine.vaultURL)
482 | let hooks = "\(name)-xpkg-hooks"
483 | let arguments = ["run", hooks, name, path, action]
484 | let result = try runner.sync(arguments: arguments, stdoutMode: .passthrough)
485 | engine.verbose.log("\n> \(hooks) \(name) \(path) \(action)")
486 | if result.status == 0 {
487 | engine.verbose.log("stdout: \(result.stdout)")
488 | engine.verbose.log("stderr: \(result.stderr)\n")
489 | return true
490 | }
491 |
492 | if !result.stdout.contains("no exexcutable product") {
493 | engine.verbose.log("failure: \(result.status)")
494 | engine.verbose.log("stdout: \(result.stdout)")
495 | engine.verbose.log("stderr: \(result.stderr)\n")
496 | }
497 |
498 | } catch {
499 | engine.output.log("Couldn't run action \(action).")
500 | engine.verbose.log(error)
501 | throw error
502 | }
503 |
504 | engine.verbose.log("Ignoring \(name) as it isn't an xpkg package.")
505 | return false
506 | }
507 | }
508 |
509 | extension Package: Hashable {
510 | func hash(into hasher: inout Hasher) {
511 | url.hash(into: &hasher)
512 | }
513 |
514 | static func == (lhs: Self, rhs: Self) -> Bool {
515 | return lhs.url == rhs.url
516 | }
517 | }
518 |
--------------------------------------------------------------------------------
/Sources/XPkgCore/URL+Extensions.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane on 04/08/22.
3 | // All code (c) 2022 - present day, Elegant Chaos Limited.
4 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
5 |
6 | import Foundation
7 |
8 | public extension URL {
9 | var normalizedGitURL: URL {
10 | let urlNoGitExtension = pathExtension == "git" ? deletingPathExtension() : self
11 | let string = urlNoGitExtension.absoluteString.replacingOccurrences(of: "git@github.com:", with: "https://github.com/")
12 | return URL(string: string)!
13 | }
14 |
15 | var asPackageSpec: String? {
16 | let components = normalizedGitURL.pathComponents
17 | let count = components.count
18 | guard count > 1 else { return nil }
19 | let specComponents = [components[count - 2], components[count - 1]]
20 | return specComponents.joined(separator: "/")
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import XPkgTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += XPkgTests.__allTests()
7 |
8 | XCTMain(tests)
9 |
--------------------------------------------------------------------------------
/Tests/XPkgTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | #if !canImport(ObjectiveC)
2 | import XCTest
3 |
4 | extension XPkgTests {
5 | // DO NOT MODIFY: This is autogenerated, use:
6 | // `swift test --generate-linuxmain`
7 | // to regenerate.
8 | static let __allTests__XPkgTests = [
9 | ("testName", testName),
10 | ("testNameOrg", testNameOrg),
11 | ("testRepo", testRepo),
12 | ]
13 | }
14 |
15 | public func __allTests() -> [XCTestCaseEntry] {
16 | return [
17 | testCase(XPkgTests.__allTests__XPkgTests),
18 | ]
19 | }
20 | #endif
21 |
--------------------------------------------------------------------------------
/Tests/XPkgTests/XPkgTests.swift:
--------------------------------------------------------------------------------
1 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
2 | // Created by Sam Deane, 08/06/2018.
3 | // All code (c) 2018 - present day, Elegant Chaos Limited.
4 | // For licensing terms, see http://elegantchaos.com/license/liberal/.
5 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
6 |
7 | import XCTest
8 | import CommandShell
9 | @testable import XPkgCore
10 |
11 | class XPkgTests: XCTestCase {
12 | var matched = false
13 |
14 | func validator(expecting: String) -> Engine.RepoValidator {
15 | self.matched = false
16 | return { url in
17 | if url.absoluteString == expecting {
18 | self.matched = true
19 | return "1.0.0"
20 | } else {
21 | return nil
22 | }
23 | }
24 | }
25 |
26 | func testName() {
27 | let engine = Engine(options: CommandShellOptions())
28 | engine.defaultOrgs = ["testorg"]
29 | let _ = engine.remotePackageURL("test", validator: validator(expecting: "git@github.com:testorg/test"))
30 | XCTAssertTrue(self.matched)
31 | }
32 |
33 | func testNameOrg() {
34 | let engine = Engine(options: CommandShellOptions())
35 | let _ = engine.remotePackageURL("someorg/someproj", validator: validator(expecting: "git@github.com:someorg/someproj"))
36 | XCTAssertTrue(self.matched)
37 | }
38 |
39 | func testRepo() {
40 | let engine = Engine(options: CommandShellOptions())
41 | let _ = engine.remotePackageURL("git@mygit.com:someorg/someproj", validator: validator(expecting: "git@mygit.com:someorg/someproj"))
42 | XCTAssertTrue(self.matched)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------