Check my dotfiles as a live example
It's a single shell script designed to install programs and personal configs in bulk with minimal config. Depending on your setup, it's capable of installing every program you use with their configurations from a fresh install.
The programs and configurations are stored in dotmodules
which is just a
directory with scripts and folders named in a conventional manner. The names
of these files are the configuration itself. If I'd use a configuration file
it would require mentioning the directories you want to interact with. I've
cut out the config and store every metadata in filenames. More on this in the
dotmodules section.
For example, using this script and an extensive set of dotmodules, on a fresh arch install after downloading my dotfiles repository I can just issue this command:
pont +arch
And then it will install every single thing I specified there without assistance. In order.
But that could be done by having a single set of applications in an install script, and all my config in a repository, right?
Yes, but what if you use multiple setups at once? Or you'd like to configure just only a few things on a remote server? Why would you add your X config to WSL? What if you distro hop a lot because you're experimenting?
But even if you use it for a single system it does have it's advantages. Separating everything into it's little bundles helps keeping track of things. If you wan't to tweak some configurations you only have to go to one place, and you get to see all related files in one module. You can see what other modules you made this one depend on.
Configuration is just one part. But oftentime things require a little setup. Downloading a repository, compilation, installation. Installing and enabling services. These things can also be in an install script. You don't have to remember things. With these scripts you can always replay the setup you did.
A modular design allows you to focus on one thing at once, and organize your configuration.
It's my first shell script project so please leave issues, if you have any
This project was originally named dot
but there is a graphviz
utility
by that name already. Another candidate was
dotman
but there are dozens of dotman
repositories already.
But pont
was free and it means dot
in hungarian, and bridge in french.
Which I guess makes sense given the heavy use of symlinks.
A mostly POSIX compatible shell on
/bin/sh
to run pont
itself. bash
and zsh
will work fine. It was
developed on dash
and it does not utilize local variables.
A key component of pont
is stow
which handles the symlinks between the
modules and the target, but it's not needed to run pont
itself and execute
modules that do not require linking. Meaning on a fresh install pont
can set
this dependency up for you if you have a module that deals with this and do
not utilize linking.
pont
works but has somewhat limited capabilities when running on a Windows
POSIX compatibility layer such as MINGW
. But is usable as a conditional,
dependency tree resolving script runner.
Windows Support is limited as I only use basic functinalities and is not tested beyond that.
Caveats on Windows:
- Stow is not available (It would be disabled even if it were as it would just cause problems)
- Limited to running PowerShell scripts with
*.ps1
file extension. - Since the PowerShell scripts are not running directly from PowerShell
a shebang is needed in every script:
#!/usr/bin/env powershell
- Has to be invoked from an elevated environment (For example
Git Bash
when running withRun As Administrator
) manually if the scrips you try to run require this. - Scripts must not have
.root.
in it's second segment becausesudo
is not available in these environments andpont
would try to invoke it when a scripts demands privilege elevation.
This enables modules to be installed on both kind of systems without conflict or excessive configuration. Most of the time they would be mutually exclusive so you would either need a lot of
conditionfiles
(To limit modules into certain OS') or condition everything in it to windows or not windows. Disabling automatic symlinking on Windows altogether is the easier choice. If needed you can always write the PowerShell script to do the symlinking. Same thing with PowerShell scripts on linux, since only the file extension is checked, you can just give it something else and have the same shebang.
This repository can be used to download and install pont
. It can also act
as a dotmodule itself. You can inclide this repository as a submodule to your
dotfiles repo.
TODO: Make online installation script, let it download itself.
If you have this repository as a submodule among your modules, you can install
it with itself. It's linking it to ~/.local/bin/pont
and if it has root
privileges then /usr/local/bin/pont
too, with the manpage.
~/.dotfiles/modules/pont/pont.sh pont
This repository also contains zsh
autocompletions
If you're using Antidote then add
this entry to your plugin file to get pont
autocompletions:
alexaegis/pont
The zsh plugin does not put
pont
to the path as using both solution would cause confusion
Autocompletions will list all presets, tags and modules when triggered. When
typing a -
, triggering the autocompletion will list all flags.
Whether you chose to put it on your path or not, using it is the same.
If you decide to use pont
in to manage your dotfiles, please add pont
as
a tag!
It does not require any configuration, but most things can be configured
through a pontrc
file which is read from every common configuration directory
in this order:
${XDG_CONFIG_HOME:-"$HOME/.config"}/pont/pontrc
${XDG_CONFIG_HOME:-"$HOME/.config"}/pontrc
$HOME/.pontrc
./.pontrc
The configuration file is a standard shell script and will be simply sourced
in pont
after the defaults have been set but before the flags have been set
from the command line. Meaning you can override the defaults.
To see what you can set, check the script, search for the sourcing of the
config file. Everything above that is overridable.
These ones can be only configured through the files, or by hand, when
executing pont
like PONT_TARGET="./target" pont <module1> <module2>...
# The default target of module packages
PONT_TARGET=${PONT_TARGET:-"$HOME"}
# Your dotfiles location. Not directly utilized, only through the next two
DOTFILES_HOME=${DOTFILES_HOME:-"$HOME/.dotfiles"}
# I suggest keeping these relative to DOTFILES_HOME
# Dotmodules will be searched here. Only direct descendant folders.
DOT_MODULES_FOLDER=${DOT_MODULES_FOLDER:-"$DOTFILES_HOME/modules"}
# Presets will be searched from here, recursively
DOT_PRESETS_FOLDER=${DOT_PRESETS_FOLDER:-"$DOTFILES_HOME/presets"}
# Modules to always include regardless of selection
DOT_BASE_MODULES="base sys"
# Will always remove broken symlinks after execution in PONT_TARGET
PONT_CLEAN_SYMLINKS=1
# Makes it always execute `chmod u+x` on '.*.(sh|zsh|bash|fish|dash)' files in
# the modules. I use it so when making scripts I don't have to deal with it.
PONT_FIX_PERMISSIONS=1
You can optionally set flags, then as much modules/presets/tags as you want.
pont [-flags] modules...
For reference you can also check the template directory, which containes examples and short description on every available file or some of my modules.
When installing a dotmodule successfully, a .tarhash
file will be created
in the modules directory. (Should be .gitignored). This containes a hash
calculated from the content of the module at the time of install. This enables
pont
to skip any module that is already installed and
has not changed since. This is especially important when installing
modules with large dependency trees.
The
.tarhash
file marks a module installed.
Modules that are already installed can be forced to be reinstalled using the
-f
or --force
flags. It just makes it ignore the hashfile.
Installed modules can be listed and sort
ed using the -I
or
--list-installed
flags. These flags makes pont
exit immediately.
pont -I
Tip: use wc -l
to count the result of these queries
pont -I | wc -l
Available modules can be listed and sort
ed using the -A
or
--list-modules
flags. These flags make pont
exit immediately.
pont -A
Deprecated modules (Explicitly marked as deprecated) can be listed and
sort
ed using the -D
or --list-deprecated
flags.
These flags make pont
exit immediately.
pont -D
Outdated modules (Modules with changed hash since their last installation)
can be listed and sort
ed using the -O
or --list-outdated
flags.
These flags make pont
exit immediately.
Since this command re-hashes all install modules it can take a while
pont -O
Every module can have 3 kinds of files inside, each of them being optional. An empty module is a valid module too.
Using script selection flags, install scripts are getting automatically excluded from execution but re-adding the
-x
flag after these, these scripts can be re-enabled.
These scripts are what supposed to install packages for your module to work. And they will run by default when installing a module.
They are not the only way to describe the installation process of a module. You can use a
Makefile
with aninstall
target. Both will execute, so either only create one type of installation or set a flag to disable one of them. See the Makefiles section for more.
Since package management is unique to each system, and not just by having
different commands to install a package. Often their name is different. Or
on a particular distribution you need to install more, or different packages,
or having to add PPA
s on debian systems etc etc.
So instead of making an overcomplicated solution I let the modules decide
how to install something. This gives great control since you're the one making
the script, pont
just executes them.
I may make an easy install for simple cases but this solution will stay as it can be used for anything, not just installation
These scripts can also be used to do some other things, like copying a
configuration file into /etc
Their names can be separated into up to 4 segments, separated by 3 periods.
Everything after the 3rd period is unused. At least 3 segments (two periods in
the filename) is needed for pont
to recognize it as a runnable script. So
you can add other, non-managed scripts in the module folder when needed. But
you can also just put them into a separate folder.
* If you skip the dependency segment then the extension will be read as the dependency, but since it's
sh
anyway it will always be true.
1.user.sh
The first segment is used for ordering. If multiple scripts are in one order, they are considered a script group.
The only mechanic related to this are fallback
scripts, more on that in the
third segment
0.root.sh
is the privilege and it can be two things:
- user
- root
No matter whether you run pont
with sudo or not, if it has to run a user
script it will always sudo
back to $SUDO_USER
(If it has to) and it will
always sudo
into root
(While keeping your environment) if it's running
root
scripts.
This makes sure that your HOME
folder will only contain items owned by you,
and not root
.
sudo
will be executed as many time it needs to as I described it above most distributions have a timeout enabled by default but if not, prepare to write your password in a few times.
In every script, you can also be sure that .
means the root of that module
as pont
will cd
into the module before executing anything.
Using the -nr
or --no-root
flags, scripts with sudo
privileges can be
skipped.
If you are on a system where you don't have
root
access, but the programs are installed and you only need your configurations, you can set this flag permamently in apontrc
, and only use thestow
ing mechanism ofpont
. The variable controlling this isroot
and is1
by default.
0.root.pacman.sh
While modules can have module dependencies, scripts can also have conditions
which can be executable dependencies and variable dependencies, if prefixed
with $
. If it is, and that variable is set and not empty, it will be
executed. If it's not prefixed with $
, it will be checked that
command -v
returns something for it or not. If yes, it will be executed.
Currently you can only specify 1 dependency on 1 script.
My common usecase for this is checking package managers. So I can have
a separate install script for pacman
systems, apt
systems and xbps
systems. And each will only execute on their respective platforms.
For variables, it's for things that cant be checked by a simple presence of
an executable. Like if I want to run a script only on wsl
, or arch
I can
name my script like 1.user.$wsl.sh
.
0.user.fallback.sh
There is an extra special dependency setting called fallback
, and this is
where script groups
come into play. If in a script group (Defined by a
common ordering segment) none got executed, fallback
will. If fallback
is
alone it will also be executed.
A usecase for this is the AUR. A lot of packages are available there, so on
arch
systems you can probably install anyting usingpacman
or with an AUR helper. If that package is only available on AUR, and it can't be installed with any other package managers, you can try compile it from source or download it using a custom installer script that they provide. Or install it with a different program. (Like Rust programs can be installed usingcargo install
, ornode
programs withnpm -g
) But instead of having a separate script for each system, or a custom script that skips onpacman
systems, just have afallback
script.
init.user.sh
Scripts starting with init
instead of the ordering first segment are run
before any other script do. And if it's user
privileged it will not just
run, but it will be sourced so everything that it defines will be available
later.
Thorough every module after that
These scripts are run before stowing the stow packages so
they can manually create folders that you don't want stow to
fold.
These are usually folders that you want to interact with from multiple
packages. Like ~/.config/
, ~/.local/bin
or ~/.config/systemd/user
.
If you're installing two modules, both to stow to the same path, where there is no directory,
stow
for the first module would no create a directory, just a single, folded symlink. The second module then would just not stow.There is, in theory a tree unfolding mechanism in
stow
, but it didn't work for me. It's maybe just because I can't use it in these conditions because it does not know of the original package that put that folded symlink there in the first place.
There can also be some special scripts that are not executed during standard installation. They are identified by their special first segment and they can be enabled using flags.
remove.sudo.sh
Using the -r
flag, scripts with remove
as their first segment will be run.
This also causes pont
to unstow
every stow package from the module,
and also removes the .tarhash
file, marking the module uninstalled.
Specifying the flag twice causes it to also run scripts that start with an r
update.sudo.sh
using the -u
flag, scripts with update
as their first segment will be run.
Non installed modules can't be updated. This won't expand the dependency graph
and only the mentioned modules will be updated. You can force expanding with
the -e
flag after the -u
though to update every dependency too.
Alternatively create a
Makefile
with anupdate
target.
There are some files that are used for configuration but they are really simple and do not follow a common format
This is meant for simpler modules where
root
is not needed and direct dependencies are not used. For more granular installation (while it can be implemented inside theMakefile
) the regular Installation scripts are much easier to use.
Makefiles provide an alternative or complementary mode of defining the
installation, update and remove procedures using make targets.
If there is a Makefile
in the module, it will be executed if make
is
available, and running makefiles are enabled. (It is by default)
They will always execute after the regular installation scripts and
are always executed using user
privileges.
.dependencies
This file lists all the dependencies the module has. It supports comments,
lines starting with #
.
Every other line is a dependency and the first word can be the name of a
module if its not prefixed with anything. It can also be a
tag if prefixed with :
or a preset
if with +
. More about those in their own sections.
The same format is used in presets too
Simple DFS
When installing a module, first every single dependency it has will be installed before. It avoids circular dependencies by simply stopping at entries that are already executed and moves on.
Using the
-sm
or--show-modules
flag, instead of installing (can be turned back on) you only get the list of modules that are gonna be executed
Modules listed in the PONT_BASE_MODULES
variable (space separated) are always
treated as selected. This can be used to define global dependencies.
Base modules are placed at the beginning of selected modules, so they are
executed earlier.
Dependencies can be conditional. After the dependency statement you can put
a ?
. Everything after that will be eval
d and then test
ed. If it's not
true, the dependency will be skipped.
An example usecase for this would be the counterpart of the
fallback section. There I defined how that module should build
in case no other install scripts can install it. With conditional dependencies
I can also list those that are required only for the fallback, only when
the fallback will actually run. If a rust program can be installed with
pacman
on arch based systems and on any other systems you want to use
cargo
you'll end up with 2 install scripts like so:
0.root.pacman.sh
0.user.cargo.sh
and at least one dependency entry:
rust ? [ ! $pacman ]
This tells pont
only install the rust
module while installing this module
when there is no pacman
available.
There are some pre calculated variables for these use-cases but you can use anything, and you can expand it with your
pontrc
as it will be sourced frompont
Some of them:
# Package managers
pacman
apt
xbps
# Init systems
systemd
# Distributions
distribution # name in /etc/os-release
arch # if distribution = 'Arch Linux', and so on
void
debian
ubuntu
fedora
.condition
Sometimes a module doesn't makes sense in an environment, but is used by
many others as a non-essential dependency. In a headless environment like
wsl
, fonts are like this. It makes no sense to install fonts in wsl
. But
some of your modules might end up depending on them for convinience.
Instead of marking each dependency with a condition, the module itself can be.
For this, create a file name .condition
in the modules root. It's content
will simply be eval
d before executing anything in the module. So not even
init
will run if .condition
is false.
.tags
This file also supports comments, and each line defines a tag. This is used to define a module group on module level.
Tags can be installed using the :
prefix like so:
pont :shell
This will install every module that has a .tags
file with the line shell
.
Tags can both appear in .dependencies
files and in *.preset
files.
Available tags can be listed and sort
ed using the -T
or --list-tags
flags. These flag makes pont
immediately exit.
pont -T
Every directory directly in a module that ends with .<MODULE_NAME>
is
a stow package.
So in a module named
zsh
, the.zsh
directory is a stowable package.
Just like scripts, stow package names are too divided by periods into segments. The last one as mentioned is for marking a directory as a stow package.
By default, stow packages will be stowed to PONT_TARGET
(can be overriden
in a pontrc
file) which is just HOME
by default.
To make stowing more dynamic, stow modules can have variables before the .
in their names. These variables will then be expanded. If it's an absolute
path it will be treated as such (Ignoring PONT_TARGET
) but if its a relative
path (it doesn't start with /
) it will be appended after PONT_TARGET
.
This path then will be used as the final target to stow to.
This variable can be set in the
init
script too if you wan't to be module specific. These scripts are run before stowing and everything they define is available during the installation of the module.
Just like packages, the second segment can be prefixed with $
. In this case
pont
checks if that variable is set or not. If it's not prefixed, it will
check with command -v
if that it's a valid executable.
Presets basically standalone dependency files without anything to install.
They have to have a .preset
extension and they are searched under
$PONT_PRESETS_FOLDER
which by default the presets
directory in your
dotfiles directory.
They can handle everything a normal dependency file can.
You can reference a preset with the +
prefix. If you have a preset called
shells.preset
, you can install it like so:
pont +shells
Presets can be included in other presets and dependency files the same way
.dependencies
# this modules dependencies are all the shells I'm using
+shells
# and my vim setup
vim
Available presets can be listed and sort
ed using the -P
or
--list-presets
flags. These flags makes pont
immediately exit.
pont -P
Having a dependency list, (A preset or a module with a .dependencies file) if you have a condition for each entry so that they are mutually exclusive, you can create an entity thats sole purpose is to conditionally redirect the dependency resolution.
One such usecase would be to have a base sys
module/preset that has
dependencies on platform specific modules like sys-debian
, sys-arch
etc,
with conditions so that they only run on their respective platforms.
You can further simplify it be having
.condition
files in the modules, which is a stronger assurance that the module will only be installed on the correct platform. In this case the junction dependency list doesn't even need conditions!
Then, other modules only have to reference this on entity, it will be resolved to the correct one.
If a script doesn't want to run, check if it has execute permissions.
stat script.sh
# or
ls -l script.sh
Or let pont
automatically fix them by using the -X
or
--toggle-fix-permissions
flags. Or by setting the
PONT_FIX_PERMISSIONS=
variable manually to 1
in your environment or
pontrc
file.
Once it's done, I might do a Rust rewrite for easier implementation of paralell execution while respecting the dependency tree. Which could be done in a script too but having all of the outputs and logs managed would be hard. The dotmodule "specification" won't really change, but it can expand.