archlinux 0.0.0 → 0.1.0

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