diff options
author | Simon Parri <simonparri@ganzeria.com> | 2025-06-10 22:40:46 -0500 |
---|---|---|
committer | Simon Parri <simonparri@ganzeria.com> | 2025-06-10 22:40:46 -0500 |
commit | d7661d34cd16cfc536631cf606a120b7c6d3053c (patch) | |
tree | c7fd1798fc6540a0f36bc7d2ef4b64e78c3078aa | |
download | dotdot-d7661d34cd16cfc536631cf606a120b7c6d3053c.tar.gz dotdot-d7661d34cd16cfc536631cf606a120b7c6d3053c.zip |
Add initial version
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README.org | 12 | ||||
-rw-r--r-- | core.rb | 85 | ||||
-rwxr-xr-x | dotdot | 75 | ||||
-rw-r--r-- | stdlib.rb | 54 |
5 files changed, 227 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/README.org b/README.org new file mode 100644 index 0000000..f52a687 --- /dev/null +++ b/README.org @@ -0,0 +1,12 @@ +#+TILTE: dotdot +#+SUBTITLE: Programmatic dotfile manager + +* Synopsis +=dotdot= was born as a dotfile manager (though it could be used for many purposes). Fundamentally, its job is: +1. to evaluate a recipe describing e.g. how to set up =~/=, +2. check if the recipe can be executed safely, e.g. that there are no files that would be overwritten, that all necessary directories exist, +3. execute the recipe. +* Usage +Currently, =dotdot= is only intended for my personal use, so I haven’t taken time to document it or even rigorously test it. =dotdot= is provided /as is/, and currently you must read the code to understand how it works. + +I intend to write documentation for it someday, but it’s too currently too early for that. @@ -0,0 +1,85 @@ +class Op + def check + raise "#{self.class} doesn't have a (required) 'check' method" + end + def do + raise "#{self.class} doesn't have a (required) 'do' method" + end + def undo + raise "#{self.class} doesn't have a (required) 'undo' method" + end +end + +def defop(name, args, &block) + name = name.to_sym + eval "class #{name} < Op; end" # Must use `eval' to access + cls = eval "#{name}" # the global namespace + cls.instance_eval do + args.map {|a| attr_reader a.to_sym } + end + cls.define_method :initialize do |**kwargs| + args.each {|arg| instance_variable_set :"@#{arg}", kwargs[arg.to_sym] } + end + cls.define_method :to_s do inspect end + cls.define_method :to_str do to_s end + cls.instance_eval &block +end + +defop :Write, [:file, :contents] do + define_method :check do + if File.exist? @file + if (File.read @file) != @contents + raise "File `#{@file}' exists but has unexpected contents" + else true + end + end + end + define_method :do do + write @file, @contents + end + define_method :undo do + rm @file + end +end + +defop :Deploy, [:ops, :save] do + define_method :initialize do |save: nil, &block| + @ops = [] + @save = save + self.instance_eval &block + end + define_method :check do + @ops.all? {|op| op.check } and + (@save ? self.save.check : true) + end + define_method :do do + self.load.undo if @save && self.save.check + @ops.each {|op| op.check || op.do } + self.save.do if @save + end + define_method :undo do + @ops.reverse.each {|op| op.check && op.undo } + self.save.undo if @save + end + define_method :save do + (Write.new file: @save, + contents: self.to_yaml) + end + define_method :load do + YAML.load_file( + @save, + permitted_classes: ([Op] + Op.subclasses)) + end +end + +def deploy(...) + Deploy.new(...) +end + +def defdeploy(name, &block) + Deploy.define_method name, &block +end + +defdeploy :op do |cls, **args| + @ops.push cls.new(**args) +end @@ -0,0 +1,75 @@ +#!/usr/bin/ruby +%w[socket fileutils yaml] + .map { require _1 } + +HOME = ENV["HOME"] or File.expand_path "~/" +HOSTNAME = Socket.gethostname + +class Binding + def eval_file(path) + self.eval (File.read path) + end +end + +def eval_file(file) + file = File.expand_path file + dir = File.dirname file + binding.eval_file file +end + +def ls(dir) + Dir.entries(dir).reject {|d| [".", ".."].include? d } +end + +def ln(from, to) + FileUtils.ln_s from, to +end + +def write(file, contents) + File.write file, contents +end + +def rm(file) + FileUtils.rm file +end + +def mkdir(dir) + FileUtils.mkdir_p dir +end + +def rmdir(dir) + FileUtils.rmdir dir +end + +require_relative "core" +require_relative "stdlib" + +$cmds = { + show: lambda {|op| + if op.is_a? Deploy + puts op.ops + else + puts op + end + }, + debug: lambda {|op| + puts op.to_yaml + }, + check: lambda {|op| + if op.check + puts "nothing left to do" + else + puts "not fully deployed" + end + }, + do: lambda {|op| op.do }, + undo: lambda {|op| op.undo }, +} +def main + command = ARGV.shift.to_sym + file = ARGV.shift || "#{HOSTNAME}.rb" + op = eval_file file + $cmds[command][op] +end + +main if __FILE__ == $0 diff --git a/stdlib.rb b/stdlib.rb new file mode 100644 index 0000000..3b414ce --- /dev/null +++ b/stdlib.rb @@ -0,0 +1,54 @@ +defop :MkDir, [:dir] do + define_method :check do + File.directory? @dir + end + define_method :do do + mkdir @dir + end + define_method :undo do + rmdir @dir if File.exist? @dir and Dir.empty? @dir + end + define_method :inspect do + "#<MkDir '#{@dir}'>" + end +end + +defop :SymLink, [:from, :to] do + define_method :check do + if File.exist? @to or File.symlink? @to # broken symlink should be kept + if !(File.symlink? @to) or (File.readlink @to) != @from + raise "File '#{@to}' exists but is not a symlink to '#{@from}'" + else true + end + end + end + define_method :do do + ln @from, @to + end + define_method :undo do + rm @to + end + define_method :inspect do + "#<SymLink '#{@from}' -> '#{to}>" + end +end + +defdeploy :tree do |src:, dst:, ignores: [/~$/], stops: []| + sp, dp = [src, dst].map {|p| File.expand_path p } + src = lambda {|path| File.join sp, path } + dst = lambda {|path| File.join dp, path } + recur = lambda do |path| + return if ignores.any? {|pat| path =~ pat } + if File.directory?(src[path]) + if stops.include? path + op SymLink, from: src[path], to: dst[path] + else + op MkDir, dir: dst[path] + (ls src[path]).each {|p| recur[File.join path, p] } + end + else + op SymLink, from: src[path], to: dst[path] + end + end + (ls sp).each {|path| recur[path] } +end |