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 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
@@ -0,0 +1,5 @@
1
+ require_relative 'core'
2
+ require_relative 'utils'
3
+ require_relative 'declarations'
4
+
5
+ Signal.trap("INT") { exit } # Suppress stack trace on Ctrl-C
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
@@ -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: []