archlinux 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +110 -0
- data/archlinux.gemspec +9 -0
- data/lib/archlinux.rb +5 -0
- data/lib/core.rb +72 -0
- data/lib/declarations.rb +243 -0
- data/lib/utils.rb +31 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f95fc88d318bc67d0abe5281c5eb940ed50e024f9f064f3af4c3d6a72edbcc87
|
4
|
+
data.tar.gz: 55e6b4029e5920f2e659f4cf4c0e532cc1216f7570bb9064836284027697d397
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cf9d3e93c200ec678bcb960dddb13c16fa534387104e22a02f64d0a66f1bd740c92bcf1fd318cbe4cd7c8864bece2ca709b5acdee667cb196ce9a106f9453542
|
7
|
+
data.tar.gz: d7b4b04d3412daf906d20fb94c7f4d9451dc4007dae41b3c23c3d22c857a92e0a5b2e7c4e63f709a8a32f41e255ec532c795d1328e75c5fe749c88a42c51cbe3
|
data/README.md
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# Archlinux
|
2
|
+
|
3
|
+
> [!WARNING]
|
4
|
+
> this can break your system, don't use it on your running system
|
5
|
+
|
6
|
+
A ruby API to manage the state of an Archlinux system.
|
7
|
+
|
8
|
+
* This project is early alpha
|
9
|
+
* It aims to have a DSL like NixOS for Archlinux
|
10
|
+
* It allow declaring the state of the system then it applies that to the current system
|
11
|
+
* The idea is to have simple and user friendly API to declare everything, system and user related
|
12
|
+
|
13
|
+
## Getting started
|
14
|
+
|
15
|
+
The project is a Ruby Gem. Create a ruby file and require the gem:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
require 'bundler/inline'
|
19
|
+
|
20
|
+
gemfile do
|
21
|
+
source "https://rubygems.org"
|
22
|
+
|
23
|
+
gem "archlinux", github: "emad-elsaid/archlinux"
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
Use `linux` function to define your system state, for example:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
linux do
|
31
|
+
hostname 'earth'
|
32
|
+
|
33
|
+
timedate timezone: 'Europe/Berlin',
|
34
|
+
ntp: true
|
35
|
+
|
36
|
+
locale "en_US.UTF-8"
|
37
|
+
keyboard keymap: 'us',
|
38
|
+
layout: "us,ara",
|
39
|
+
model: "",
|
40
|
+
variant: "",
|
41
|
+
options: "ctrl:nocaps,caps:lctrl,ctrl:swap_lalt_lctl,grp:alt_space_toggle"
|
42
|
+
|
43
|
+
package %w[
|
44
|
+
linux
|
45
|
+
linux-firmware
|
46
|
+
linux-headers
|
47
|
+
base
|
48
|
+
base-devel
|
49
|
+
bash-completion
|
50
|
+
pacman-contrib
|
51
|
+
docker
|
52
|
+
locate
|
53
|
+
syncthing
|
54
|
+
]
|
55
|
+
|
56
|
+
service %w[
|
57
|
+
docker
|
58
|
+
NetworkManager
|
59
|
+
]
|
60
|
+
|
61
|
+
timer 'plocate-updatedb'
|
62
|
+
|
63
|
+
user 'smith', groups: ['wheel', 'docker'] do
|
64
|
+
aur %w[
|
65
|
+
kernel-install-mkinitcpio
|
66
|
+
google-chrome
|
67
|
+
]
|
68
|
+
|
69
|
+
service %w[
|
70
|
+
ssh-agent
|
71
|
+
syncthing
|
72
|
+
]
|
73
|
+
|
74
|
+
copy './user/.config/.', '/home/smith/.config'
|
75
|
+
end
|
76
|
+
|
77
|
+
firewall :syncthing
|
78
|
+
|
79
|
+
on_finalize do
|
80
|
+
sudo 'bootctl install'
|
81
|
+
sudo 'reinstall-kernels'
|
82
|
+
end
|
83
|
+
|
84
|
+
file '/etc/X11/xorg.conf.d/40-touchpad.conf', <<~EOT
|
85
|
+
Section "InputClass"
|
86
|
+
Identifier "libinput touchpad catchall"
|
87
|
+
MatchIsTouchpad "on"
|
88
|
+
MatchDevicePath "/dev/input/event*"
|
89
|
+
Driver "libinput"
|
90
|
+
Option "Tapping" "on"
|
91
|
+
Option "NaturalScrolling" "true"
|
92
|
+
EndSection
|
93
|
+
EOT
|
94
|
+
|
95
|
+
replace '/etc/mkinitcpio.conf', /^(.*)base udev(.*)$/, '\1systemd\2'
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
Now you can run the script with ruby as root:
|
100
|
+
|
101
|
+
```shell
|
102
|
+
sudo ruby <script-name.rb>
|
103
|
+
```
|
104
|
+
|
105
|
+
|
106
|
+
It will do the following:
|
107
|
+
- Install missing packages, remove any other package
|
108
|
+
- Make sure services and timers are running
|
109
|
+
- Do other configurations like locale, X11 keyboard settings, hostname
|
110
|
+
- Ensure users are created and in specified groups
|
data/archlinux.gemspec
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.authors = ["Emad Elsaid"]
|
3
|
+
s.homepage = "https://github.com/emad-elsaid/archlinux"
|
4
|
+
s.files = `git ls-files`.lines.map(&:chomp)
|
5
|
+
s.name = 'archlinux'
|
6
|
+
s.summary = "Archlinux DSL to manage whole system state"
|
7
|
+
s.version = '0.0.0'
|
8
|
+
s.licenses = ["GPL-3.0-or-later"]
|
9
|
+
end
|
data/lib/archlinux.rb
ADDED
data/lib/core.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# ==============================================================
|
2
|
+
# CORE:
|
3
|
+
# State of the system It should hold all the information we need to build the
|
4
|
+
# system, packages, files, changes...etc. everything will run inside an instance
|
5
|
+
# of this class
|
6
|
+
# ==============================================================
|
7
|
+
class State
|
8
|
+
def apply(block)
|
9
|
+
instance_eval &block
|
10
|
+
end
|
11
|
+
|
12
|
+
# Run block on prepare step. id identifies the block uniqueness in the steps.
|
13
|
+
# registering a block with same id multiple times replaces old block by new
|
14
|
+
# one. if id is nil the block location in source code is used as an id
|
15
|
+
def on_prepare(id=nil, &block)
|
16
|
+
id ||= caller_locations(1,1).first.to_s
|
17
|
+
@prepare_steps ||= {}
|
18
|
+
@prepare_steps[id] = block
|
19
|
+
end
|
20
|
+
|
21
|
+
# Same as on_prepare but for install step
|
22
|
+
def on_install(id=nil, &block)
|
23
|
+
id ||= caller_locations(1,1).first.to_s
|
24
|
+
@install_steps ||= {}
|
25
|
+
@install_steps[id] = block
|
26
|
+
end
|
27
|
+
|
28
|
+
# Same as on_prepare but for configure step
|
29
|
+
def on_configure(id=nil, &block)
|
30
|
+
id ||= caller_locations(1,1).first.to_s
|
31
|
+
@configure_steps ||= {}
|
32
|
+
@configure_steps[id] = block
|
33
|
+
end
|
34
|
+
|
35
|
+
# Same as on_finalize but for configure step
|
36
|
+
def on_finalize(id=nil, &block)
|
37
|
+
id ||= caller_locations(1,1).first.to_s
|
38
|
+
@finalize_steps ||= {}
|
39
|
+
@finalize_steps[id] = block
|
40
|
+
end
|
41
|
+
|
42
|
+
# Run all registered code blocks in the following order: Prepare, Install, Configure, Finalize
|
43
|
+
def run_steps
|
44
|
+
if @prepare_steps&.any?
|
45
|
+
log "=> Prepare"
|
46
|
+
@prepare_steps.each { |_, step| apply(step) }
|
47
|
+
end
|
48
|
+
|
49
|
+
if @install_steps&.any?
|
50
|
+
log "=> Install"
|
51
|
+
@install_steps.each { |_, step| apply(step) }
|
52
|
+
end
|
53
|
+
|
54
|
+
if @configure_steps&.any?
|
55
|
+
log "=> Configure"
|
56
|
+
@configure_steps.each { |_, step| apply(step) }
|
57
|
+
end
|
58
|
+
|
59
|
+
if @finalize_steps&.any?
|
60
|
+
log "=> Finalize"
|
61
|
+
@finalize_steps.each { |_, step| apply(step) }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# passed block will run in the context of a State instance and then a builder
|
67
|
+
# will build this state
|
68
|
+
def linux(&block)
|
69
|
+
s = State.new
|
70
|
+
s.apply(block)
|
71
|
+
s.run_steps
|
72
|
+
end
|
data/lib/declarations.rb
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
# ==============================================================
|
5
|
+
# DECLARATIONS:
|
6
|
+
# Functions the user will run to declare the state of the system
|
7
|
+
# like packages to be present, files, services, user, group...etc
|
8
|
+
# ==============================================================
|
9
|
+
|
10
|
+
# Install a package on install step and remove packages not registered with this
|
11
|
+
# function
|
12
|
+
def package(*names)
|
13
|
+
names.flatten!
|
14
|
+
@packages ||= Set.new
|
15
|
+
@packages += names.map(&:to_s)
|
16
|
+
|
17
|
+
# install step to install packages required and remove not required
|
18
|
+
on_install do
|
19
|
+
# install packages list as is
|
20
|
+
names = @packages.join(" ")
|
21
|
+
log "Installing packages", packages: @packages
|
22
|
+
sudo "pacman --noconfirm --needed -S #{names}" unless @packages.empty?
|
23
|
+
|
24
|
+
# expand groups to packages
|
25
|
+
group_packages = Set.new(`pacman --quiet -Sg #{names}`.lines.map(&:strip))
|
26
|
+
|
27
|
+
# full list of packages that should exist on the system
|
28
|
+
all = @packages + group_packages
|
29
|
+
|
30
|
+
# actual list on the system
|
31
|
+
installed = Set.new(`pacman -Q --quiet --explicit --unrequired --native`.lines.map(&:strip))
|
32
|
+
|
33
|
+
unneeded = installed - all
|
34
|
+
next if unneeded.empty?
|
35
|
+
|
36
|
+
log "Removing packages", packages: unneeded
|
37
|
+
sudo("pacman -Rs #{unneeded.join(" ")}")
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
# aur command to install packages from aur on install step
|
43
|
+
def aur(*names)
|
44
|
+
names.flatten!
|
45
|
+
@aurs ||= Set.new
|
46
|
+
@aurs += names.map(&:to_s)
|
47
|
+
|
48
|
+
on_install do
|
49
|
+
names = @aurs || []
|
50
|
+
log "Install AUR packages", packages: names
|
51
|
+
cache = "./cache/aur"
|
52
|
+
FileUtils.mkdir_p cache
|
53
|
+
Dir.chdir cache do
|
54
|
+
names.each do |package|
|
55
|
+
system("git clone --depth 1 --shallow-submodules https://aur.archlinux.org/#{package}.git") unless Dir.exists?(package)
|
56
|
+
Dir.chdir package do
|
57
|
+
system("makepkg --syncdeps --install --noconfirm --needed")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# set timezone and NTP settings during prepare step
|
65
|
+
def timedate(timezone: 'UTC', ntp: true)
|
66
|
+
@timedate = {timezone: timezone, ntp: ntp}
|
67
|
+
|
68
|
+
on_configure do
|
69
|
+
log "Set timedate", @timedate
|
70
|
+
sudo "timedatectl set-timezone #{@timedate[:timezone]}"
|
71
|
+
sudo "timedatectl set-ntp #{@timedate[:ntp]}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# enable system service if root or user service if not during finalize step
|
76
|
+
def service(*names)
|
77
|
+
names.flatten!
|
78
|
+
@services ||= Set.new
|
79
|
+
@services += names.map(&:to_s)
|
80
|
+
|
81
|
+
on_finalize do
|
82
|
+
log "Enable services", services: @services
|
83
|
+
user_flags = root? ? "" : "--user"
|
84
|
+
|
85
|
+
system "systemctl enable #{user_flags} #{@services.join(" ")}"
|
86
|
+
|
87
|
+
# Disable services that were enabled manually and not in the list we have
|
88
|
+
services = `systemctl list-unit-files #{user_flags} --state=enabled --type=service --no-legend --no-pager`
|
89
|
+
enabled_manually = services.lines.map{|l| l.strip.split(/\s+/) }.select{|l| (l[1] == 'enabled') && (l[2] == 'disabled')}
|
90
|
+
names_without_extension = enabled_manually.map{|l| l.first.delete_suffix(".service") }
|
91
|
+
to_disable = names_without_extension - @services.to_a
|
92
|
+
|
93
|
+
next if to_disable.empty?
|
94
|
+
|
95
|
+
log "Services to disable", packages: to_disable
|
96
|
+
# system "systemctl disable #{user_flags} #{to_disable.join(" ")}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# enable system timer if root or user timer if not during finalize step
|
101
|
+
def timer(*names)
|
102
|
+
names.flatten!
|
103
|
+
@timers ||= Set.new
|
104
|
+
@timers += names.map(&:to_s)
|
105
|
+
|
106
|
+
on_finalize do
|
107
|
+
log "Enable timers", timers: @timers
|
108
|
+
timers = @timers.map{ |t| "#{t}.timer" }.join(" ")
|
109
|
+
if root?
|
110
|
+
sudo "systemctl enable #{timers}"
|
111
|
+
else
|
112
|
+
system "systemctl enable --user #{timers}"
|
113
|
+
end
|
114
|
+
# disable all other timers
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# set keyboard settings during prepare step
|
119
|
+
def keyboard(keymap: nil, layout: nil, model: nil, variant: nil, options: nil)
|
120
|
+
@keyboard ||= {}
|
121
|
+
values = {
|
122
|
+
keymap: keymap,
|
123
|
+
layout: layout,
|
124
|
+
model: model,
|
125
|
+
variant: variant,
|
126
|
+
options: options
|
127
|
+
}.compact
|
128
|
+
@keyboard.merge!(values)
|
129
|
+
|
130
|
+
on_prepare do
|
131
|
+
next unless @keyboard[:keymap]
|
132
|
+
|
133
|
+
sudo "localectl set-keymap #{@keyboard[:keymap]}"
|
134
|
+
|
135
|
+
m = @keyboard.to_h.slice(:layout, :model, :variant, :options)
|
136
|
+
sudo "localectl set-x11-keymap \"#{m[:layout]}\" \"#{m[:model]}\" \"#{m[:variant]}\" \"#{m[:options]}\""
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def locale(value)
|
141
|
+
@locale = value
|
142
|
+
|
143
|
+
on_prepare do
|
144
|
+
sudo "localectl set-locale #{@locale}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# create a user and assign a set of group. if block is passes the block will run
|
149
|
+
# in as this user. block will run during the configure step
|
150
|
+
def user(name, groups: [], &block)
|
151
|
+
name = name.to_s
|
152
|
+
|
153
|
+
@user ||= {}
|
154
|
+
@user[name] ||= {}
|
155
|
+
@user[name][:groups] ||= []
|
156
|
+
@user[name][:groups] += groups.map(&:to_s)
|
157
|
+
@user[name][:state] = State.new
|
158
|
+
@user[name][:state].apply(block) if block_given?
|
159
|
+
|
160
|
+
on_configure do
|
161
|
+
@user.each do |name, conf|
|
162
|
+
exists = Etc.getpwnam name rescue nil
|
163
|
+
sudo "useradd #{name}" if exists
|
164
|
+
sudo "usermod --groups #{groups.join(",")} #{name}" if groups.any?
|
165
|
+
|
166
|
+
fork do
|
167
|
+
currentuser = Etc.getpwnam(name)
|
168
|
+
Process::GID.change_privilege(currentuser.gid)
|
169
|
+
Process::UID.change_privilege(currentuser.uid)
|
170
|
+
ENV['XDG_RUNTIME_DIR'] = "/run/user/#{currentuser.uid}"
|
171
|
+
conf[:state].run_steps
|
172
|
+
end
|
173
|
+
|
174
|
+
Process.wait
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Copy src inside dest during configure step, if src/. will copy src content to dest
|
180
|
+
def copy(src, dest)
|
181
|
+
@copy ||= []
|
182
|
+
@copy << { src: src, dest: dest }
|
183
|
+
|
184
|
+
on_configure do
|
185
|
+
next unless @copy
|
186
|
+
next if @copy.empty?
|
187
|
+
|
188
|
+
@copy.each do |item|
|
189
|
+
log "Copying", item
|
190
|
+
FileUtils.cp_r item[:src], item[:dest]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Replace a regex pattern with replacement string in a file during configure step
|
196
|
+
def replace(file, pattern, replacement)
|
197
|
+
@replace ||= []
|
198
|
+
@replace << {file: file, pattern: pattern, replacement: replacement}
|
199
|
+
|
200
|
+
on_configure do
|
201
|
+
@replace.each do |params|
|
202
|
+
input = File.read(params[:file])
|
203
|
+
output = input.gsub(params[:pattern], params[:replacement])
|
204
|
+
File.write(params[:file], output)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# setup add ufw enable it and allow ports during configure step
|
210
|
+
def firewall(*allow)
|
211
|
+
@firewall ||= Set.new
|
212
|
+
@firewall += allow.map(&:to_s)
|
213
|
+
|
214
|
+
package :ufw
|
215
|
+
service :ufw
|
216
|
+
|
217
|
+
on_configure do
|
218
|
+
sudo "ufw allow #{@firewall.join(' ')}"
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# Write a file during configure step
|
223
|
+
def file(path, content)
|
224
|
+
@files ||= {}
|
225
|
+
@files[path] = content
|
226
|
+
|
227
|
+
on_configure do
|
228
|
+
@files.each do |path, content|
|
229
|
+
File.write(path, content)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def hostname(name)
|
235
|
+
@hostname = name
|
236
|
+
|
237
|
+
file '/etc/hostname', "#{@hostname}\n"
|
238
|
+
|
239
|
+
on_configure do
|
240
|
+
log "Setting hostname", hostname: @hostname
|
241
|
+
sudo "hostnamectl set-hostname #{@hostname}"
|
242
|
+
end
|
243
|
+
end
|
data/lib/utils.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'etc'
|
2
|
+
|
3
|
+
# Prints a message to the STDOUT
|
4
|
+
#
|
5
|
+
# @param [String] msg a log message to print
|
6
|
+
#
|
7
|
+
# @param [Hash<String, Object>] args prints each key and value in separate lines after message
|
8
|
+
def log(msg, args={})
|
9
|
+
puts msg
|
10
|
+
|
11
|
+
return unless args.any?
|
12
|
+
|
13
|
+
max = args.keys.map(&:to_s).max_by(&:length).length
|
14
|
+
args.each do |k, v|
|
15
|
+
vs = case v
|
16
|
+
when Array, Set
|
17
|
+
"(#{v.length}) " + v.join(", ")
|
18
|
+
else
|
19
|
+
v
|
20
|
+
end
|
21
|
+
puts "\t#{k.to_s.rjust(max)}: #{vs}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def root?
|
26
|
+
Process.uid == Etc.getpwnam('root').uid
|
27
|
+
end
|
28
|
+
|
29
|
+
def sudo(command)
|
30
|
+
root? ? system(command) : system("sudo #{command}")
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: archlinux
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Emad Elsaid
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-02-18 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- README.md
|
20
|
+
- archlinux.gemspec
|
21
|
+
- lib/archlinux.rb
|
22
|
+
- lib/core.rb
|
23
|
+
- lib/declarations.rb
|
24
|
+
- lib/utils.rb
|
25
|
+
homepage: https://github.com/emad-elsaid/archlinux
|
26
|
+
licenses:
|
27
|
+
- GPL-3.0-or-later
|
28
|
+
metadata: {}
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options: []
|
31
|
+
require_paths:
|
32
|
+
- lib
|
33
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
requirements: []
|
44
|
+
rubygems_version: 3.3.25
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: Archlinux DSL to manage whole system state
|
48
|
+
test_files: []
|