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 +archAnd 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
*.ps1file 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 Bashwhen running withRun As Administrator) manually if the scrips you try to run require this. - Scripts must not have
.root.in it's second segment becausesudois not available in these environments andpontwould 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 pontThis repository also contains zsh autocompletions
If you're using Antidote then add
this entry to your plugin file to get pont autocompletions:
alexaegis/pontThe zsh plugin does not put
pontto 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
./.pontrcThe 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=1You 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
.tarhashfile 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 sorted using the -I or
--list-installed flags. These flags makes pont exit immediately.
pont -ITip: use wc -l to count the result of these queries
pont -I | wc -lAvailable modules can be listed and sorted using the -A or
--list-modules flags. These flags make pont exit immediately.
pont -ADeprecated modules (Explicitly marked as deprecated) can be listed and
sorted using the -D or --list-deprecated flags.
These flags make pont exit immediately.
pont -DOutdated modules (Modules with changed hash since their last installation)
can be listed and sorted 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 -OEvery 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
-xflag 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
Makefilewith aninstalltarget. 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 PPAs 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
shanyway 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.
sudowill 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
rootaccess, but the programs are installed and you only need your configurations, you can set this flag permamently in apontrc, and only use thestowing mechanism ofpont. The variable controlling this isrootand is1by 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
archsystems you can probably install anyting usingpacmanor 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, ornodeprograms withnpm -g) But instead of having a separate script for each system, or a custom script that skips onpacmansystems, just have afallbackscript.
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,
stowfor 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
Makefilewith anupdatetarget.
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
rootis 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
-smor--show-modulesflag, 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 evald and then tested. 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.shand 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
pontrcas 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 evald 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 :shellThis 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 sorted using the -T or --list-tags
flags. These flag makes pont immediately exit.
pont -TEvery directory directly in a module that ends with .<MODULE_NAME> is
a stow package.
So in a module named
zsh, the.zshdirectory 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
initscript 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 +shellsPresets 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
vimAvailable presets can be listed and sorted using the -P or
--list-presets flags. These flags makes pont immediately exit.
pont -PHaving 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
.conditionfiles 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.shOr 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.