archlinux 0.0.0

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