archlinux 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|