Compare commits

...

8 Commits

Author SHA1 Message Date
janishutz 2fd69cc595 feat(init): Templates handling 2026-04-25 16:43:53 +02:00
janishutz 541a876307 feat(config): Schema validator, initial config merger setup 2026-04-23 11:31:01 +02:00
janishutz 5126e0373f feat(PKGBUILD): First setup 2026-04-23 11:30:31 +02:00
janishutz 299ac49b40 feat(schema): Mostly finish the config options 2026-04-18 14:48:52 +02:00
janishutz b6901b59e6 feat(schema): More described opts 2026-04-17 16:57:07 +02:00
janishutz bcd0339d88 feat: Start config schema 2026-04-17 15:41:07 +02:00
janishutz 4017e0263b chore: Note 2026-04-17 11:33:51 +02:00
janishutz b71d18cdc9 chore: Notes 2026-04-16 16:42:22 +02:00
21 changed files with 562 additions and 7 deletions
+28
View File
@@ -0,0 +1,28 @@
# Maintainer: Janis Hutz <development@janishutz.com>
pkgname=archmgr-git
pkgver=0.0.0
pkgrel=1
pkgdesc='A nixos-like declarative config and package manager for Arch Linux'
arch=('any')
url="https://github.com/janishutz/archmgr"
license=('GPL3')
depends=('python', 'python-pyaml')
makedepends=('git')
provides=('archmgr')
conflicts=('archmgr')
source=("$pkgname"::git+${url}.git)
sha256sums=('SKIP') # TODO: Add?
pkgver() {
cd "${pkgname}"
# TODO: For the non-git pkgbuild, need to use different output
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
package() {
cd "${pkgname}"
# TODO: Need to finish
}
+2
View File
@@ -9,6 +9,7 @@ import commands.init as init
import commands.pull as pull
import commands.push as push
import commands.prepare as setup
from config import load_config
if __name__ == "__main__":
args, ap = cliargs.add_cli_args()
@@ -29,6 +30,7 @@ if __name__ == "__main__":
\\___/
""")
print(load_config("config.yml"))
try:
if args.cmd == "commit":
commit.commit(args.force, args.no_render)
+3 -2
View File
@@ -17,5 +17,6 @@ def commit(force: bool = False, no_render: bool = False):
# Probably do that check in the pacman util lib tho
add, remove = pkg_diff([], pacman.list_explicitly_installed())
print_diff(add, remove)
if confirm(True, "Do you really want to proceed?"):
pw = password()
if confirm(False, "Do you really want to proceed?"):
pacman.install_package_list(add)
pacman.uninstall_package_list(remove)
+14 -5
View File
@@ -1,19 +1,28 @@
import os
import shutil
import commands.util.git as git
from commands.util.input_mgr import confirm
def init(force: bool = False):
dir = os.getcwd()
script_dir = os.path.dirname(os.path.realpath(__file__))
if force:
[os.remove(dir) for dir in os.listdir(dir)]
if confirm(False, "Do you really want to IRREVERSIBLY DELETE the contents of this folder and redo setup?"):
[os.remove(file) for file in os.listdir(dir)]
git.init(dir)
os.mkdir(dir + "/config")
os.mkdir(dir + "/etc")
with open(dir + "/config.yaml") as file:
file.write("")
os.mkdir(dir + "/db")
os.mkdir(dir + "/system")
os.mkdir(dir + "/includes")
shutil.copy(script_dir + "/templates/config/config.yml", dir + "/config.yml")
shutil.copy(script_dir + "/templates/config/system/", dir + "/includes/system/")
shutil.copy(script_dir + "/templates/config/templates/", dir + "/includes/templates/")
shutil.copy(script_dir + "/templates/README.md", dir + "/README.md")
print("Initialized a new archmgr repository")
# TODO: For the files, store the permissions in a db
# TODO: Warn user to not delete .config/archmgr repo
# TODO: Set up that repo (where to put it? /usr/share?)
# TODO: Consider collecting function
# TODO: Consider collecting function -> If no files present, will only collect the pkgs, else also the files
# TODO: Config folder instead of single config file
# TODO: Also store the folder name of the config folder in that repo (needs to be easily changeable for user!)
+2
View File
@@ -26,3 +26,5 @@ def setup():
return
print("==> Installation completed")
# TODO: Check if yay is available before installing
+384
View File
@@ -0,0 +1,384 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object",
"properties": {
"requires": {
"type": "array",
"items": {
"type": "string",
"pattern": "^([a-zA-Z0-9.-_]+\\/)+",
"description": "Path to other configs, relative to this file (e.g. config/pkgs.yaml will expand to dirname(this_file)/config/pkgs.yaml)"
},
"description": "Imports for other config files that will be merged into this one. Precedence order is bottom up (i.e. lowest has highest precedence)"
},
"pkgs": {
"type": "object",
"description": "The packages to be installed",
"properties": {
"individual": {
"type:": "array",
"description": "the packages to be installed, by their package name",
"items": {
"type": "string",
"pattern": "^[a-z0-9-._]+(?=[a-z0-9]$)"
}
},
"repos": {
"type": "object",
"properties": {
"enabled_repos": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"description": "The repos to set up",
"properties": {
"name": {
"type": "string",
"description": "The repositories to set up",
"pattern": "^(core|extra|core-testing|extra-testing|multilib|multilib-testing|[a-z0-9-]+(?=[a-z0-9]$))"
},
"setup_cmds": {
"type": "array",
"description": "The commands to run to set it up (optional if any of the explicitly supported ones)",
"items": {
"type": "string",
"description": "Command to run"
}
},
"mirrors": {
"type": "object",
"description": "Configure the mirrors to use",
"properties": {
"use_default": {
"type": "boolean",
"description": "Whether to use the default mirrors or not",
"default": true
},
"extra_mirrors": {
"type": "array",
"description": "Any extra mirrors you want to add. At least one mirror needs to be put here if use_default is false. Order matters",
"items": {
"type": "string"
}
}
}
}
}
}
},
"reflector": {
"type": "object",
"description": "Use reflector to update the mirrors",
"properties": {
"enabled": {
"type": "boolean",
"default": false
},
"interval": {
"type": "number",
"description": "The number of days to elapse between reflector reruns"
},
"countries": {
"type": "array",
"description": "The countries in which the should be located (only applies for the main arch repos)",
"items": {
"type": "string",
"pattern": "^[A-Z][a-z]*[a-z]$"
},
"maxItems": 5,
"minItems": 1
},
"count": {
"type": "number",
"description": "The number of mirrors to add to the list",
"maximum": 20,
"minimum": 3
}
},
"required": [
"enabled"
],
"additionalProperties": false
},
"additionalProperties": false
}
},
"bundles": {
"type": "array",
"description": "Bundled packages, installing all the recommended extra software for them (such as hyprland and nvim)",
"maxItems": 1,
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The bundle name",
"pattern": "^[a-z0-9-._]+(?=[a-z0-9]$)"
},
"ignored_pkgs": {
"type": "array",
"description": "List of packages from the bundle that should not be installed",
"items": {
"type": "string",
"pattern": "^[a-z0-9-._]+(?=[a-z0-9]$)"
}
}
},
"additionalProperties": false,
"required": [
"name"
]
}
}
},
"additionalProperties": false
},
"users": {
"type": "array",
"description": "Users to add, including groups. Users will be diffed and removed if they are removed from here. No files are deleted",
"items": {
"type": "object",
"properties": {
"username": {
"type": "string",
"pattern": "^[a-zA-Z0-9\\-._]{2,19}(?=[a-zA-Z0-9]$)"
},
"groups": {
"type": "array",
"description": "The groups to add the user to. Groups are created if they don't exist. User's own group doesn't have to be listed explicitly",
"items": {
"type": "string",
"pattern": "^[a-zA-Z0-9\\-._]{2,19}(?=[a-zA-Z0-9]$)"
}
},
"sudo_user": {
"type": "boolean",
"default": false,
"description": "Whether a user can use sudo or not. Same as appending them to the `wheel` group"
},
"home_dir": {
"type": "boolean",
"description": "Whether to create a home directory for the user or not",
"default": false
}
},
"required": [
"username"
],
"additionalProperties": false
}
},
"boot": {
"type": "object",
"description": "Settings for the bootloader, such as theme, using os-prober, etc",
"properties": {
"bootloader": {
"type": "string",
"description": "The bootloader to use (more coming eventually)",
"pattern": "^(grub)"
},
"esp_dir": {
"type": "string",
"description": "The directory for the bootloader files. Has to end in slash",
"pattern": "^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))\\/$"
},
"theme": {
"type": "object",
"description": "Configuration for the bootloader theme",
"properties": {
"folder": {
"type": "string",
"description": "Where the folder for the theme is found. Can be relative to the config repo or file system and has to end in slash",
"pattern": "^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))\\/$"
}
},
"additionalProperties": false,
"required": [
"folder"
]
},
"os_prober": {
"type": "boolean",
"description": "Whether to enable OS prober to search for other operating systems"
}
},
"additionalProperties": false,
"required": [
"bootloader",
"esp_dir"
]
},
"themes": {
"type": "object",
"properties": {}
},
"git": {
"type": "object",
"description": "Automatically set up credential manager and clone repos",
"properties": {
"creds": {
"type": "object",
"description": "Which git services to log into",
"properties": {
"manager": {
"type": "string",
"description": "The git credential manager to use. Set to none if you don't want one (default)",
"pattern": "^(git-credential-manager|none)",
"default": "none"
}
},
"additionalProperties": false
},
"repos": {
"type": "array",
"description": "Which repos to clone (removing one from here doesn't delete it from the system and only pulls if folder does not exist)",
"items": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "git clone URL (ssh or HTTP(S))",
"pattern": "^((https?):\\/\\/(([a-z0-9-]+)((?=\\.))\\.)+[a-z]+(?=\\/)\\/([\\w\\-?.=]+(?=\\/[\\w\\-?.=])\\/)*([\\w\\-?&.=\\/]+(?=[\\w\\-.=\\/]$)))|([a-zA-Z0-9\\-._]+(?=[a-zA-Z0-9])[a-zA-Z0-9])@(([a-z0-9\\-]+(?=\\.))\\.)+[a-z]+(?=:):([a-zA-Z0-9\\-._]+(?=\\/)\\/)+([a-zA-Z0-9\\-._]+(?=[a-zA-Z0-9]$))"
},
"clone_path": {
"type": "string",
"description": "The location on the local system where to put it. Must be absolute path. Parent folders will be created if they don't exist",
"pattern": "^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))\\/$"
}
},
"additionalProperties": false,
"required": [
"url",
"clone_path"
]
}
},
"additionalProperties": false
}
},
"template_data": {
"type": "array",
"description": "The data to be inserted into the templates",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name that appears in the template"
},
"data": {
"type": "string",
"description": "The data that is to be inserted"
}
},
"required": [
"name",
"data"
],
"additionalProperties": false
}
},
"symlinks": {
"type": "array",
"description": "Symlinks to create",
"items": {
"type": "object",
"properties": {
"destination": {
"type": "string",
"description": "The directory the link should point to",
"pattern": "^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))"
},
"location": {
"type": "string",
"description": "What to call the link",
"pattern": "^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))"
}
}
}
},
"cmds": {
"type": "object",
"properties": {
"once": {
"type": "array",
"description": "Commands to run on only once (uses the name property to determine if it needs to run)",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "This name is used to track if a command was run before."
},
"cmd": {
"type": "string"
},
"hook": {
"type": "string",
"description": "Where in the execution of archmgr to run",
"default": "end",
"pattern": "^(pre-pkg|post-pkg|pre-git|post-git|end)"
},
"user": {
"type": "string",
"default": "root",
"pattern": "^[a-zA-Z0-9\\-._]{2,19}(?=[a-zA-Z0-9]$)",
"description": "The user to run as. Be aware that only the current user's password is available, unless capture_output is set to false"
},
"capture_output": {
"type": "boolean",
"default": true,
"description": "Whether or not to hide the output from the user"
}
},
"additionalProperties": false,
"required": [
"name",
"cmd"
]
}
},
"always": {
"type": "array",
"description": "Commands to run on each apply",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Used to indicate to user what command is executing. If omitted, will be truncated cmd"
},
"cmd": {
"type": "string"
},
"hook": {
"type": "string",
"description": "Where in the execution of archmgr to run",
"default": "end",
"pattern": "^(pre-pkg|post-pkg|pre-git|post-git|end)"
},
"user": {
"type": "string",
"default": "root",
"pattern": "^[a-zA-Z0-9\\-._]{2,19}(?=[a-zA-Z0-9]$)",
"description": "The user to run as. Be aware that only the current user's password is available, unless capture_output is set to false"
},
"capture_output": {
"type": "boolean",
"default": true,
"description": "Whether or not to hide the output from the user"
}
}
}
}
},
"additionalProperties": false
}
},
"required": [
"pkgs",
"boot"
],
"additionalProperties": false
}
+58
View File
@@ -0,0 +1,58 @@
# yaml-language-server: $schema=config.schema.json
# TODO: Change the above to an import URL instead
requires:
- path/to/other/configs/relative/to/this # Reads the other configs after finishing this one
pkgs:
individual:
- pkg_name
bundles:
- name: hyprland
repos:
reflector:
enabled: false
countries:
- Switzerland
users:
- username: username
groups:
- group_1
home_dir: True
boot:
bootloader: grub
esp_dir: /boot/
theme:
folder: ~/.path/to/theme/
os_prober: False
# Also copies over the /etc/default/grub config or equivalent for other supported bootloaders
# TODO: Desktops, login managers, full disk encryption etc configuration?
themes:
gtk: theme_name
qt: theme_name # or use_gtk to use the gtk theme instaed
font: Comfortaa 11 # the font name to be used (also needs to be installed)
icon_theme: candy-icons # The icon theme to use (also needs to be installed)
cursor_theme: oreo_spark_blue_cursors # TODO: Consider if GTK settings file should just be copied
git:
creds:
manager: git-credential-manager # or none
repos:
- url: https://github.com/janishutz/janishutz
clone_path: ~/projects/ # Project location will be clone_path/<repo name>
- url: git@git.janishutz.com:janishutz/nvim
clone_path: ~/projects/ # Project location will be clone_path/<repo name>
template_data:
- name: template_data_name
data: the_data
cmds:
always:
- cmd: "cmd to run every time archmgr is run"
once:
- name: "cmd1"
cmd: "cmd to run on first execution of archmgr (or if not executed previously)"
+31
View File
@@ -0,0 +1,31 @@
from typing import Any, cast
import yaml
from config import validator
from config.merger import merge_configs
def _load_config_file(file: str):
with open(file, "r") as f:
parsed = yaml.load(f, Loader=yaml.FullLoader)
return parsed
def load_config(file: str):
# Load and validate initial config
try:
loaded_conf = _load_config_file(file)
except Exception:
return {}
if not validator.validate(loaded_conf):
return {}
conf = cast(dict[str, Any], loaded_conf)
requires = cast(list[str], conf["requires"])
conf.pop("requires")
# Recursively load files
for conf_file in requires:
conf = merge_configs(conf, load_config(conf_file))
return conf
+6
View File
@@ -0,0 +1,6 @@
def merge_configs(config: dict, new_config: dict):
if len(new_config) == 0 or len(config) == 0:
return config
# Merge configs
return config
+18
View File
@@ -0,0 +1,18 @@
import json
import jsonschema
with open("config.schema.json") as file:
schema = json.load(file)
def validate(config):
try:
jsonschema.validate(config, schema)
except jsonschema.SchemaError:
print("Schema invalid")
return False
except jsonschema.ValidationError:
print("Config invalid")
return False
return True
+12
View File
@@ -18,3 +18,15 @@
- [ ] Dynamic selection of more configs (i.e. require syntax)
- [ ] Own config syntax?
- [ ] Autocompletion
- [ ] Basic arch install how? -> Probably manual (or semi-automatic)
- [ ] Mounts?
# REGEX
TODO: Improve the below (especially file can be shortened with positive lookahead)
- File: `^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))`
- Folder: `^(^(~|\\.|\\.\\.)?\\/)?([\\w\\/.-]+(?!.*[^\\w\\/.-]+))\\/$`
- URL (just domain): `^(https?):\\/\\/(([a-z0-9-]+)((?=\\.))\\.)+[a-z]+(?=[a-z]$)`
- Full URL: `^(https?):\\/\\/(([a-z0-9-]+)((?=\\.))\\.)+[a-z]+(?=\\/)\\/([\\w\\-?.=]+(?=\\/[\\w\\-?.=])\\/)*([\\w\\-?&.=\\/]+(?=[\\w\\-.=\\/]$))`
- UNIX username and groups: `^[a-zA-Z0-9\\-._]{2,19}(?=[a-zA-Z0-9]$)`
- Git SSH: `([a-zA-Z0-9\\-._]+(?=[a-zA-Z0-9])[a-zA-Z0-9])@(([a-z0-9\\-]+(?=\\.))\\.)+[a-z]+(?=:):([a-zA-Z0-9\\-._]+(?=\\/)\\/)+([a-zA-Z0-9\\-._]+(?=[a-zA-Z0-9]$))`
+1
View File
@@ -1,3 +1,4 @@
pyyaml
pylette
argcomplete
inquirer
+3
View File
@@ -0,0 +1,3 @@
# archmgr data folder
archmgr is a nixos-inspired package and config manager for Arch Linux.
To function, it needs both a configuration file (or multiple) and
View File
View File
View File
View File
View File
View File
View File