archlinux 0.0.0 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f95fc88d318bc67d0abe5281c5eb940ed50e024f9f064f3af4c3d6a72edbcc87
4
- data.tar.gz: 55e6b4029e5920f2e659f4cf4c0e532cc1216f7570bb9064836284027697d397
3
+ metadata.gz: c5a6439dbf4a18b2f03c70c67dd3707dbe0aa860e147bbb86242a5540173fe09
4
+ data.tar.gz: 2b56713549a7861cf2602e3132f66c94a66d10419de67fe89ea973a32724e6bc
5
5
  SHA512:
6
- metadata.gz: cf9d3e93c200ec678bcb960dddb13c16fa534387104e22a02f64d0a66f1bd740c92bcf1fd318cbe4cd7c8864bece2ca709b5acdee667cb196ce9a106f9453542
7
- data.tar.gz: d7b4b04d3412daf906d20fb94c7f4d9451dc4007dae41b3c23c3d22c857a92e0a5b2e7c4e63f709a8a32f41e255ec532c795d1328e75c5fe749c88a42c51cbe3
6
+ metadata.gz: 69423c894c111c99ca618d9b86e17f10e7b040edd8e2facb06fa6dd599fb37d1b4011162da28a13dc73e2f15859437c36cb5284e1b0e393f25c014da5543ea9d
7
+ data.tar.gz: 4823b44bfdb3ca749657bf81d1b003cd5abb72f08d822d5880890a8cd6b561855f05f5df46b618b48ede8cbe3874f999e3f2372016d50e200a988ca3c73196bf
data/.rubocop.yml ADDED
@@ -0,0 +1,22 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 3.0
4
+
5
+ Style/StringLiterals:
6
+ Enabled: false
7
+ Style/FrozenStringLiteralComment:
8
+ Enabled: false
9
+ Style/StringLiteralsInInterpolation:
10
+ Enabled: false
11
+ Metrics/MethodLength:
12
+ Max: 100
13
+ Lint/ShadowingOuterLocalVariable:
14
+ Enabled: false
15
+ Style/RescueModifier:
16
+ Enabled: false
17
+ Metrics/AbcSize:
18
+ Enabled: false
19
+ Metrics/CyclomaticComplexity:
20
+ Enabled: false
21
+ Metrics/PerceivedComplexity:
22
+ Enabled: false
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Archlinux
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/archlinux.svg)](https://badge.fury.io/rb/archlinux)
4
+
3
5
  > [!WARNING]
4
6
  > this can break your system, don't use it on your running system
5
7
 
@@ -108,3 +110,16 @@ It will do the following:
108
110
  - Make sure services and timers are running
109
111
  - Do other configurations like locale, X11 keyboard settings, hostname
110
112
  - Ensure users are created and in specified groups
113
+
114
+
115
+ # Concepts
116
+
117
+ ## Declarations:
118
+
119
+ Functions the user will run to declare the state of the system like packages to
120
+ be present, files, services, user, group...etc
121
+
122
+ ## Utilities:
123
+
124
+ Methods for logging and small predicates, technically any ruby method is a
125
+ utility. calling it executes the code directly instead of declaring a state.
data/archlinux.gemspec CHANGED
@@ -4,6 +4,8 @@ Gem::Specification.new do |s|
4
4
  s.files = `git ls-files`.lines.map(&:chomp)
5
5
  s.name = 'archlinux'
6
6
  s.summary = "Archlinux DSL to manage whole system state"
7
- s.version = '0.0.0'
8
- s.licenses = ["GPL-3.0-or-later"]
7
+ s.version = '0.1.0'
8
+ s.licenses = ["GPL-3.0-or-later"]
9
+ s.metadata['rubygems_mfa_required'] = 'true'
10
+ s.required_ruby_version = '>=3.0'
9
11
  end
@@ -0,0 +1,16 @@
1
+ require 'set'
2
+
3
+ # @group Declarations:
4
+
5
+ # setup add ufw enable it and allow ports during configure step
6
+ def ufw(*allow)
7
+ @ufw ||= Set.new
8
+ @ufw += allow.map(&:to_s)
9
+
10
+ package :ufw
11
+ service :ufw
12
+
13
+ on_configure do
14
+ sudo "ufw allow #{@ufw.join(' ')}"
15
+ end
16
+ end
data/lib/archlinux.rb CHANGED
@@ -1,5 +1,10 @@
1
+ def require_relative_dir(dir)
2
+ Dir["#{File.dirname(__FILE__)}/#{dir}/**/*.rb"].each { |f| require f }
3
+ end
4
+
1
5
  require_relative 'core'
2
6
  require_relative 'utils'
3
- require_relative 'declarations'
7
+ require_relative_dir 'declarations'
8
+ require_relative_dir 'applications'
4
9
 
5
10
  Signal.trap("INT") { exit } # Suppress stack trace on Ctrl-C
data/lib/core.rb CHANGED
@@ -1,69 +1,62 @@
1
- # ==============================================================
2
- # CORE:
3
1
  # State of the system It should hold all the information we need to build the
4
2
  # system, packages, files, changes...etc. everything will run inside an instance
5
3
  # of this class
6
- # ==============================================================
7
4
  class State
8
5
  def apply(block)
9
- instance_eval &block
6
+ instance_eval(&block)
10
7
  end
11
8
 
12
9
  # Run block on prepare step. id identifies the block uniqueness in the steps.
13
10
  # registering a block with same id multiple times replaces old block by new
14
11
  # 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
12
+ def on_prepare(id = nil, &block)
13
+ id ||= caller_locations(1, 1).first.to_s
17
14
  @prepare_steps ||= {}
18
15
  @prepare_steps[id] = block
19
16
  end
20
17
 
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
18
+ # Same as {#on_prepare} but for install step
19
+ def on_install(id = nil, &block)
20
+ id ||= caller_locations(1, 1).first.to_s
24
21
  @install_steps ||= {}
25
22
  @install_steps[id] = block
26
23
  end
27
24
 
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
25
+ # Same as {.on_prepare} but for configure step
26
+ def on_configure(id = nil, &block)
27
+ id ||= caller_locations(1, 1).first.to_s
31
28
  @configure_steps ||= {}
32
29
  @configure_steps[id] = block
33
30
  end
34
31
 
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
32
+ # Same as {.on_prepare} but for configure step
33
+ def on_finalize(id = nil, &block)
34
+ id ||= caller_locations(1, 1).first.to_s
38
35
  @finalize_steps ||= {}
39
36
  @finalize_steps[id] = block
40
37
  end
41
38
 
42
39
  # Run all registered code blocks in the following order: Prepare, Install, Configure, Finalize
43
40
  def run_steps
44
- if @prepare_steps&.any?
45
- log "=> Prepare"
46
- @prepare_steps.each { |_, step| apply(step) }
47
- end
41
+ run_step("Prepare", @prepare_steps)
42
+ run_step("Install", @install_steps)
43
+ run_step("Configure", @configure_steps)
44
+ run_step("Finalize", @finalize_steps)
45
+ end
46
+ end
48
47
 
49
- if @install_steps&.any?
50
- log "=> Install"
51
- @install_steps.each { |_, step| apply(step) }
52
- end
48
+ private
53
49
 
54
- if @configure_steps&.any?
55
- log "=> Configure"
56
- @configure_steps.each { |_, step| apply(step) }
57
- end
50
+ def run_step(name, step)
51
+ return unless step&.any?
58
52
 
59
- if @finalize_steps&.any?
60
- log "=> Finalize"
61
- @finalize_steps.each { |_, step| apply(step) }
62
- end
63
- end
53
+ log "=> #{name}"
54
+ step.each_value { |s| apply(s) }
64
55
  end
65
56
 
66
- # passed block will run in the context of a State instance and then a builder
57
+ # @group Core:
58
+
59
+ # passed block will run in the context of a {State} instance and then a builder
67
60
  # will build this state
68
61
  def linux(&block)
69
62
  s = State.new
@@ -0,0 +1,88 @@
1
+ require 'fileutils'
2
+
3
+ # @group Declarations:
4
+
5
+ # Copy src inside dest during configure step, if src/. will copy src content to dest
6
+ def copy(src, dest)
7
+ @copy ||= []
8
+ @copy << { src: src, dest: dest }
9
+
10
+ on_configure do
11
+ next unless @copy
12
+ next if @copy.empty?
13
+
14
+ @copy.each do |item|
15
+ log "Copying", item
16
+ FileUtils.cp_r item[:src], item[:dest]
17
+ end
18
+ end
19
+ end
20
+
21
+ # Replace a regex pattern with replacement string in a file during configure step
22
+ def replace(file, pattern, replacement)
23
+ @replace ||= []
24
+ @replace << { file: file, pattern: pattern, replacement: replacement }
25
+
26
+ on_configure do
27
+ @replace.each do |params|
28
+ input = File.read(params[:file])
29
+ output = input.gsub(params[:pattern], params[:replacement])
30
+ File.write(params[:file], output)
31
+ end
32
+ end
33
+ end
34
+
35
+ # link file to destination
36
+ def symlink(target, link_name)
37
+ @symlink ||= Set.new
38
+ @symlink << { target: target, link_name: link_name }
39
+
40
+ on_configure do
41
+ @symlink.each do |params|
42
+ target = File.expand_path params[:target]
43
+ link_name = File.expand_path params[:link_name]
44
+
45
+ if File.directory?(target)
46
+ log "Can't link directories", target: target, link_name: link_name
47
+ exit
48
+ end
49
+
50
+ log "Linking", target: target, link_name: link_name
51
+
52
+ # make the parent if it doesn't exist
53
+ dest_dir = File.dirname(link_name)
54
+ FileUtils.mkdir_p(dest_dir)
55
+
56
+ # link with force
57
+ FileUtils.ln_s(target, link_name, force: true)
58
+ end
59
+ end
60
+ end
61
+
62
+ # on prepare make sure the directory exists
63
+ def mkdir(*path)
64
+ path.flatten!
65
+ @mkdir ||= Set.new
66
+ @mkdir += path
67
+
68
+ on_prepare do
69
+ @mkdir.each do |path|
70
+ FileUtils.mkdir_p File.expand_path(path)
71
+ end
72
+ end
73
+ end
74
+
75
+ # Write a file during configure step
76
+ def file(path, content)
77
+ @files ||= {}
78
+ @files[path] = content
79
+
80
+ on_configure do
81
+ @files.each do |path, content|
82
+ FileUtils.mkdir_p File.dirname(path)
83
+ File.write(path, content)
84
+ rescue Errno::ENOENT => e
85
+ log "Error: Can't write file", file: path, error: e
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,22 @@
1
+ require 'set'
2
+
3
+ # @group Declarations:
4
+
5
+ # on prepare make sure a git repository is cloned to directory
6
+ def git_clone(from:, to: nil)
7
+ @git_clone ||= Set.new
8
+ @git_clone << { from: from, to: to }
9
+
10
+ on_install do
11
+ @git_clone.each do |item|
12
+ from = item[:from]
13
+ to = item[:to]
14
+ system "git clone #{from} #{to}" unless File.exist?(File.expand_path(to))
15
+ end
16
+ end
17
+ end
18
+
19
+ # git clone for github repositories
20
+ def github_clone(from:, to: nil)
21
+ git_clone(from: "https://github.com/#{from}", to: to)
22
+ end
@@ -0,0 +1,81 @@
1
+ require 'set'
2
+ require 'fileutils'
3
+
4
+ # @group Utilities
5
+
6
+ # Utility function, returns true of package is installed
7
+ def package?(name)
8
+ system("pacman -Qi #{name} &> /dev/null")
9
+ end
10
+
11
+ # @group Declarations:
12
+
13
+ # Install a package on install step and remove packages not registered with this
14
+ # function
15
+ def package(*names)
16
+ names.flatten!
17
+ @packages ||= Set.new
18
+ @packages += names.map(&:to_s)
19
+
20
+ # install step to install packages required and remove not required
21
+ on_install do
22
+ # install missing packages
23
+ need_install = @packages.reject { |p| package? p }
24
+ need_install_args = need_install.join(" ")
25
+ if need_install.any?
26
+ log "Installing packages", packages: need_install
27
+ sudo "pacman --noconfirm --needed -S #{need_install_args}"
28
+ end
29
+
30
+ # expand groups to packages
31
+ packages_args = @packages.join(" ")
32
+ group_packages = Set.new(`pacman --quiet -Sg #{packages_args}`.lines.map(&:strip))
33
+
34
+ # full list of packages that should exist on the system
35
+ all = @packages + group_packages
36
+
37
+ # actual list on the system
38
+ installed = Set.new(`pacman -Q --quiet --explicit --unrequired --native`.lines.map(&:strip))
39
+
40
+ unneeded = installed - all
41
+ next if unneeded.empty?
42
+
43
+ log "Removing packages", packages: unneeded
44
+ sudo("pacman -Rsu #{unneeded.join(" ")}")
45
+ end
46
+ end
47
+
48
+ # aur command to install packages from aur on install step
49
+ def aur(*names)
50
+ names.flatten!
51
+ @aurs ||= Set.new
52
+ @aurs += names.map(&:to_s)
53
+
54
+ on_install do
55
+ log "Install AUR packages", packages: @aurs
56
+ cache = "./cache/aur"
57
+ FileUtils.mkdir_p cache
58
+ Dir.chdir cache do
59
+ @aurs.each do |package|
60
+ unless Dir.exist?(package)
61
+ system("git clone --depth 1 --shallow-submodules https://aur.archlinux.org/#{package}.git")
62
+ end
63
+ Dir.chdir package do
64
+ pkgbuild = File.readlines('PKGBUILD')
65
+ pkgver = pkgbuild.find { |l| l.start_with?('pkgver=') }.split('=')[1].strip.chomp('"')
66
+ package_info = `pacman -Qi #{package}`.strip.lines.to_h { |l| l.strip.split(/\s*:\s*/, 2) }
67
+ installed = package_info["Version"].to_s.split("-")[0] == pkgver
68
+
69
+ system("makepkg --syncdeps --install --noconfirm --needed") unless installed
70
+ end
71
+ end
72
+ end
73
+
74
+ foreign = Set.new(`pacman -Qm`.lines.map { |l| l.split(/\s+/, 2).first })
75
+ unneeded = foreign - @aurs
76
+ next if unneeded.empty?
77
+
78
+ log "Foreign packages to remove", packages: unneeded
79
+ sudo("pacman -Rsu #{unneeded.join(" ")}")
80
+ end
81
+ end
@@ -0,0 +1,107 @@
1
+ require 'set'
2
+
3
+ # @group Declarations:
4
+
5
+ # set timezone and NTP settings during prepare step
6
+ def timedate(timezone: 'UTC', ntp: true)
7
+ @timedate = { timezone: timezone, ntp: ntp }
8
+
9
+ on_configure do
10
+ log "Set timedate", @timedate
11
+ sudo "timedatectl set-timezone #{@timedate[:timezone]}"
12
+ sudo "timedatectl set-ntp #{@timedate[:ntp]}"
13
+ end
14
+ end
15
+
16
+ # enable system service if root or user service if not during finalize step
17
+ def service(*names)
18
+ names.flatten!
19
+ @services ||= Set.new
20
+ @services += names.map(&:to_s)
21
+
22
+ on_finalize do
23
+ user_flags = root? ? "" : "--user"
24
+
25
+ services = `systemctl list-unit-files #{user_flags} --state=enabled --type=service --no-legend --no-pager`
26
+ enabled = services.lines
27
+ enabled.map! { |l| l.strip.split(/\s+/) }
28
+ enabled.each { |l| l[0].delete_suffix!(".service") }
29
+
30
+ to_enable = @services - enabled.map(&:first)
31
+
32
+ if to_enable.any?
33
+ log "Enable services", services: to_enable
34
+ system "systemctl enable #{user_flags} #{to_enable.join(" ")}"
35
+ end
36
+
37
+ # Disable services that were enabled manually and not in the list we have
38
+ enabled_manually = enabled.select! { |l| l[2] == 'disabled' }.map(&:first)
39
+
40
+ to_disable = enabled_manually - @services.to_a
41
+ next if to_disable.empty?
42
+
43
+ log "Services to disable", packages: to_disable
44
+ system "systemctl disable #{user_flags} #{to_disable.join(" ")}"
45
+ end
46
+ end
47
+
48
+ # enable system timer if root or user timer if not during finalize step
49
+ def timer(*names)
50
+ names.flatten!
51
+ @timers ||= Set.new
52
+ @timers += names.map(&:to_s)
53
+
54
+ on_finalize do
55
+ log "Enable timers", timers: @timers
56
+ timers = @timers.map { |t| "#{t}.timer" }.join(" ")
57
+ if root?
58
+ sudo "systemctl enable #{timers}"
59
+ else
60
+ system "systemctl enable --user #{timers}"
61
+ end
62
+ # disable all other timers
63
+ end
64
+ end
65
+
66
+ # set keyboard settings during prepare step
67
+ def keyboard(keymap: nil, layout: nil, model: nil, variant: nil, options: nil)
68
+ @keyboard ||= {}
69
+ values = {
70
+ keymap: keymap,
71
+ layout: layout,
72
+ model: model,
73
+ variant: variant,
74
+ options: options
75
+ }.compact
76
+ @keyboard.merge!(values)
77
+
78
+ on_prepare do
79
+ next unless @keyboard[:keymap]
80
+
81
+ sudo "localectl set-keymap #{@keyboard[:keymap]}"
82
+
83
+ m = @keyboard.to_h.slice(:layout, :model, :variant, :options)
84
+ sudo "localectl set-x11-keymap \"#{m[:layout]}\" \"#{m[:model]}\" \"#{m[:variant]}\" \"#{m[:options]}\""
85
+ end
86
+ end
87
+
88
+ # Sets locale using localectl
89
+ def locale(value)
90
+ @locale = value
91
+
92
+ on_prepare do
93
+ sudo "localectl set-locale #{@locale}"
94
+ end
95
+ end
96
+
97
+ # Sets the machine hostname
98
+ def hostname(name)
99
+ @hostname = name
100
+
101
+ file '/etc/hostname', "#{@hostname}\n"
102
+
103
+ on_configure do
104
+ log "Setting hostname", hostname: @hostname
105
+ sudo "hostnamectl set-hostname #{@hostname}"
106
+ end
107
+ end
@@ -0,0 +1,48 @@
1
+ require 'set'
2
+ require 'etc'
3
+
4
+ # @group Declarations:
5
+
6
+ # create a user and assign a set of group. if block is passes the block will run
7
+ # in as this user. block will run during the configure step
8
+ def user(name, groups: [], autologin: nil, &block)
9
+ name = name.to_s
10
+
11
+ @user ||= {}
12
+ @user[name] ||= {}
13
+ @user[name][:groups] ||= []
14
+ @user[name][:groups] += groups.map(&:to_s)
15
+ @user[name][:autologin] = autologin unless autologin.nil?
16
+ @user[name][:state] ||= State.new
17
+ @user[name][:state].apply(block) if block_given?
18
+
19
+ on_configure do
20
+ @user.each do |name, conf|
21
+ exists = Etc.getpwnam name rescue nil
22
+ sudo "useradd #{name}" unless exists
23
+ sudo "usermod --groups #{groups.join(",")} #{name}" if groups.any?
24
+
25
+ if conf[:autologin]
26
+ FileUtils.mkdir_p '/etc/systemd/system/getty@tty1.service.d'
27
+ file '/etc/systemd/system/getty@tty1.service.d/autologin.conf', <<~FILE
28
+ [Service]
29
+ ExecStart=
30
+ ExecStart=-/sbin/agetty -o '-p -f -- \\u' --noclear --autologin #{name} %I $TERM
31
+ FILE
32
+ end
33
+
34
+ fork do
35
+ currentuser = Etc.getpwnam(name)
36
+ Process::GID.change_privilege(currentuser.gid)
37
+ Process::UID.change_privilege(currentuser.uid)
38
+ ENV['XDG_RUNTIME_DIR'] = "/run/user/#{currentuser.uid}"
39
+ ENV['HOME'] = currentuser.dir
40
+ ENV['USER'] = currentuser.name
41
+ ENV['LOGNAME'] = currentuser.name
42
+ conf[:state].run_steps
43
+ end
44
+
45
+ Process.wait
46
+ end
47
+ end
48
+ end
data/lib/utils.rb CHANGED
@@ -1,11 +1,11 @@
1
1
  require 'etc'
2
2
 
3
+ # @group Utilities
4
+
3
5
  # 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={})
6
+ # @param msg [String] a log message to print
7
+ # @param args [Hash<String, Object>] prints each key and value in separate lines after message
8
+ def log(msg, args = {})
9
9
  puts msg
10
10
 
11
11
  return unless args.any?
@@ -22,10 +22,13 @@ def log(msg, args={})
22
22
  end
23
23
  end
24
24
 
25
+ # Checks if current user is the root
26
+ # @return [Boolean] true if current user is root and false otherwise
25
27
  def root?
26
28
  Process.uid == Etc.getpwnam('root').uid
27
29
  end
28
30
 
31
+ # Runs the command with sudo if current user is not root
29
32
  def sudo(command)
30
33
  root? ? system(command) : system("sudo #{command}")
31
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: archlinux
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emad Elsaid
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-18 00:00:00.000000000 Z
11
+ date: 2024-03-28 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -16,16 +16,23 @@ executables: []
16
16
  extensions: []
17
17
  extra_rdoc_files: []
18
18
  files:
19
+ - ".rubocop.yml"
19
20
  - README.md
20
21
  - archlinux.gemspec
22
+ - lib/applications/ufw.rb
21
23
  - lib/archlinux.rb
22
24
  - lib/core.rb
23
- - lib/declarations.rb
25
+ - lib/declarations/file.rb
26
+ - lib/declarations/git.rb
27
+ - lib/declarations/pacman.rb
28
+ - lib/declarations/systemd.rb
29
+ - lib/declarations/user.rb
24
30
  - lib/utils.rb
25
31
  homepage: https://github.com/emad-elsaid/archlinux
26
32
  licenses:
27
33
  - GPL-3.0-or-later
28
- metadata: {}
34
+ metadata:
35
+ rubygems_mfa_required: 'true'
29
36
  post_install_message:
30
37
  rdoc_options: []
31
38
  require_paths:
@@ -34,7 +41,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
34
41
  requirements:
35
42
  - - ">="
36
43
  - !ruby/object:Gem::Version
37
- version: '0'
44
+ version: '3.0'
38
45
  required_rubygems_version: !ruby/object:Gem::Requirement
39
46
  requirements:
40
47
  - - ">="
data/lib/declarations.rb DELETED
@@ -1,243 +0,0 @@
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