lace 0.2.1

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.
data/bin/lace ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+ std_trap = trap("INT") { exit! 130 } # no backtrace thanks
3
+ require 'pathname'
4
+
5
+ LIB_PATH = Pathname.new(__FILE__).realpath.dirname.parent.join("lib").to_s
6
+ $:.unshift(LIB_PATH)
7
+
8
+ require "lace/utils"
9
+ require "lace/exceptions"
10
+ require "lace/version"
11
+
12
+ require "extend/ARGV"
13
+ require "extend/pathname"
14
+
15
+ packages_folder = Pathname.new(ENV["HOME"]).join(".cassias")
16
+
17
+ if ENV["LACE_FOLDER"]
18
+ packages_folder = Pathname.new(ENV["LACE_FOLDER"])
19
+ end
20
+ LACE_PKGS_FOLDER = packages_folder
21
+
22
+ module Lace extend self
23
+ attr_accessor :failed
24
+ alias_method :failed?, :failed
25
+ end
26
+
27
+ ARGV.extend(LaceArgvExtension)
28
+
29
+ if ARGV.debug?
30
+ require "debugger"
31
+ end
32
+
33
+ case ARGV.first when '-h', '--help', '--usage', '-?', 'help', nil
34
+ require 'cmd/help'
35
+ puts Lace.help_s
36
+ exit ARGV.first ? 0 : 1
37
+ when '--version'
38
+ puts Lace::VERSION
39
+ exit 0
40
+ when '-v'
41
+ puts "lace #{Lace::VERSION}"
42
+ # Shift the -v to the end of the parameter list
43
+ ARGV << ARGV.shift
44
+ # If no other arguments, just quit here.
45
+ exit 0 if ARGV.length == 1
46
+ end
47
+
48
+
49
+ begin
50
+ trap("INT", std_trap) # restore default CTRL-C handler
51
+ if Process.uid.zero?
52
+ raise "Refusing to run as sudo"
53
+ end
54
+
55
+ aliases = {'ls' => 'list',
56
+ 'rm' => 'remove'}
57
+
58
+ cmd = ARGV.shift
59
+ cmd = aliases[cmd] if aliases[cmd]
60
+
61
+ if require "cmd/" + cmd
62
+ Lace.send cmd.to_s.gsub('-', '_').downcase
63
+ else
64
+ onoe "Unknown command: #{cmd}"
65
+ exit 1
66
+ end
67
+
68
+ rescue ResourceNotSpecified
69
+ abort "This command requires a resource argument"
70
+ rescue UsageError
71
+ onoe "Invalid usage"
72
+ abort ARGV.usage
73
+ rescue SystemExit
74
+ puts "Kernel.exit" if ARGV.verbose?
75
+ raise
76
+ rescue Interrupt => e
77
+ puts # seemingly a newline is typical
78
+ exit 130
79
+ rescue RuntimeError, SystemCallError => e
80
+ raise if e.message.empty?
81
+ onoe e
82
+ puts e.backtrace if false
83
+ exit 1
84
+ rescue Exception => e
85
+ onoe e
86
+ puts "#{Tty.white}Please report this bug:"
87
+ puts e.backtrace
88
+ exit 1
89
+ else
90
+ exit 1 if Lace.failed?
91
+ end
@@ -0,0 +1,12 @@
1
+
2
+ require 'lace/package'
3
+ require 'lace/exceptions'
4
+
5
+ module Lace extend self
6
+ def activate
7
+ package_name = ARGV.shift
8
+ raise ResourceNotSpecified if not package_name
9
+ PackageUtils.activate package_name, ARGV
10
+ end
11
+ end
12
+
@@ -0,0 +1,11 @@
1
+ require 'lace/package'
2
+ require 'lace/exceptions'
3
+
4
+ module Lace extend self
5
+ def deactivate
6
+ package_name = ARGV.shift
7
+ raise ResourceNotSpecified if not package_name
8
+ PackageUtils.deactivate package_name, ARGV
9
+ end
10
+ end
11
+
data/lib/cmd/fetch.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'lace/package'
2
+ require 'lace/exceptions'
3
+
4
+ module Lace extend self
5
+ def fetch
6
+ resource = ARGV.shift
7
+ raise ResourceNotSpecified if not resource
8
+ PackageUtils.fetch resource, ARGV
9
+ end
10
+ end
data/lib/cmd/help.rb ADDED
@@ -0,0 +1,39 @@
1
+ HELP = <<-EOS
2
+ Example usage:
3
+ Synopsis:
4
+ lace <cmd> <pkg-uri/name> [<flavor>] [--name=<name>] [--version] [--no-hooks]
5
+
6
+ lace ls
7
+
8
+ lace fetch <pkg-uri>
9
+ lace fetch <pkg-uri>
10
+
11
+ lace install <pkg-uri>
12
+ lace install <pkg-uri> <flavor>
13
+
14
+ lace activate <pkg-name>
15
+ lace activate <pkg-name> <flavor>
16
+
17
+ lace deactivate <pkg-name>
18
+ lace deactivate <pkg-name> <flavor>
19
+
20
+ lace remove <pkg-name>
21
+ lace update <pkg-name>
22
+
23
+ Troubleshooting:
24
+ lace help
25
+ lace info <pkg-name>
26
+ lace validate <local-directory>
27
+
28
+ For further help visit:
29
+ https://github.com/kairichard/lace
30
+ EOS
31
+
32
+ module Lace extend self
33
+ def help
34
+ puts HELP
35
+ end
36
+ def help_s
37
+ HELP
38
+ end
39
+ end
@@ -0,0 +1,56 @@
1
+ require 'erb'
2
+
3
+ require 'lace/package'
4
+ require 'lace/exceptions'
5
+
6
+ INSPECT = <<-EOS
7
+ Inspection of simple:
8
+ active: <%= package.is_active? %>
9
+ flavors: <%= package.flavors %>
10
+ version: <%= package.version %>
11
+ upgradeable: <%= package.upgradeable? %>
12
+ manifest: <%= package.manifest %>
13
+ EOS
14
+
15
+ module Lace extend self
16
+ def inspect
17
+ resource = ARGV.shift
18
+ raise ResourceNotSpecified if not resource
19
+ package = PackagePresenter.new Package.new(resource, false)
20
+ puts ERB.new(INSPECT).result(binding)
21
+ end
22
+ end
23
+
24
+ class PackagePresenter
25
+ attr_accessor :pkg
26
+
27
+ def initialize obj
28
+ @pkg = obj
29
+ end
30
+
31
+ def is_active?
32
+ pkg.is_active?
33
+ end
34
+
35
+ def flavors
36
+ flavors_as_string.empty? ? "nil" : flavors_as_string
37
+ end
38
+
39
+ def flavors_as_string
40
+ if @pkg.facts.flavors
41
+ return @pkg.facts.flavors.join ", "
42
+ end
43
+ end
44
+
45
+ def version
46
+ @pkg.facts.version or 'n/a'
47
+ end
48
+
49
+ def upgradeable?
50
+ @pkg.is_git_repo?
51
+ end
52
+
53
+ def manifest
54
+ return @pkg.facts.facts_file
55
+ end
56
+ end
@@ -0,0 +1,10 @@
1
+ require 'lace/package'
2
+ require 'lace/exceptions'
3
+
4
+ module Lace extend self
5
+ def install
6
+ resource = ARGV.shift
7
+ raise ResourceNotSpecified if not resource
8
+ PackageUtils.install resource, ARGV
9
+ end
10
+ end
data/lib/cmd/list.rb ADDED
@@ -0,0 +1,36 @@
1
+ require 'lace/package'
2
+
3
+ module Lace extend self
4
+
5
+ def linked_files
6
+ home_dir = ENV["HOME"]
7
+ Dir.foreach(home_dir).map do |filename|
8
+ next if filename == '.' or filename == '..'
9
+ File.readlink File.join(home_dir, filename) if File.symlink? File.join(home_dir, filename)
10
+ end.compact.uniq
11
+ end
12
+
13
+ def active_dotties
14
+ linked_files.map do |path|
15
+ Pathname.new File.dirname(path)
16
+ end.uniq
17
+ end
18
+
19
+ def installed_dotties
20
+ Dir.glob(File.join(LACE_PKGS_FOLDER, "**")).sort.map do |p|
21
+ Pathname.new(p).basename.to_s
22
+ end
23
+ end
24
+
25
+ def list
26
+ if installed_dotties.length > 0
27
+ installed_dotties.map do |d|
28
+ package = Package.new d, false
29
+ puts "- [#{Tty.green}#{package.is_active? ? "*" : " "}#{Tty.reset}] #{d}"
30
+ end
31
+ else
32
+ puts "There are no kits installed"
33
+ end
34
+ end
35
+
36
+ end
data/lib/cmd/remove.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'lace/package'
2
+ require 'lace/exceptions'
3
+
4
+ module Lace extend self
5
+ def remove
6
+ package_name = ARGV.shift
7
+ raise ResourceNotSpecified if not package_name
8
+ PackageUtils.remove package_name, ARGV
9
+ end
10
+ end
11
+
data/lib/cmd/update.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'lace/package'
2
+ require 'lace/exceptions'
3
+
4
+ module Lace extend self
5
+ def update
6
+ resource = ARGV.shift
7
+ raise ResourceNotSpecified if not resource
8
+ PackageUtils.update resource, ARGV
9
+ end
10
+ end
@@ -0,0 +1,124 @@
1
+ require 'erb'
2
+
3
+ require 'lace/package'
4
+ require 'lace/utils'
5
+ require 'lace/exceptions'
6
+
7
+ VALIDATE = <<-EOS
8
+ Lace-Manifest Validation Report:
9
+ <% validation.errors.each do |error| -%>
10
+ <%= "%-58s [ %s ]" % [error[0] + ':', error[1]] %>
11
+ <% unless error[2].nil? -%>
12
+ <% error[2].each do |line| -%>
13
+ <%= Tty.gray %><%= '# '+line.to_s %><%= Tty.reset %>
14
+ <% end -%>
15
+ <% end -%>
16
+ <% end -%>
17
+ EOS
18
+
19
+ module Lace extend self
20
+ def validate
21
+ resource = ARGV.shift
22
+ raise ResourceNotSpecified if not resource
23
+ validation = PackageValidator.new Facts.new(resource), ARGV.shift
24
+ puts ERB.new(VALIDATE, nil, '-').result(binding)
25
+ Lace.failed = true if validation.has_errors?
26
+ end
27
+ end
28
+
29
+ class PackageValidator
30
+ attr_accessor :errors
31
+
32
+ class << self
33
+ attr_accessor :validations
34
+ def validate name, method_name
35
+ @validations ||= []
36
+ @validations << [name, method_name]
37
+ end
38
+ end
39
+
40
+ validate 'config-files', :config_files_present
41
+ validate 'version', :version_present
42
+ validate 'homepage', :homepage_present
43
+ validate 'post-install hook', :post_install_hooks_ok
44
+ validate 'post-update hook', :post_update_hooks_ok
45
+
46
+ def initialize facts, flavor=nil
47
+ @facts = facts
48
+ @errors = []
49
+ if @facts.has_flavors? && flavor.nil?
50
+ raise RuntimeError.new FlavorArgumentMsg % @facts.flavors.join("\n- ")
51
+ elsif @facts.has_flavors? && flavor != false
52
+ @facts.flavor! flavor
53
+ end
54
+ validate
55
+ end
56
+
57
+ def check_hooks hook_cmd
58
+ hook_cmd.map do |cmd|
59
+ if !File.exist? cmd
60
+ "#{cmd} cannot be found"
61
+ elsif !File.executable? cmd
62
+ "#{cmd} is not executable"
63
+ end
64
+ end.compact
65
+ end
66
+
67
+ def hook_ok hook
68
+ hook_cmd = @facts.post(hook)
69
+ if hook_cmd.empty?
70
+ ["#{Tty.green}skipped#{Tty.reset}", nil]
71
+ else
72
+ errors = check_hooks hook_cmd
73
+ if errors.length > 0
74
+ ["#{Tty.red}error#{Tty.reset}", errors]
75
+ else
76
+ ["ok", nil]
77
+ end
78
+ end
79
+ end
80
+
81
+ def post_install_hooks_ok
82
+ hook_ok :install
83
+ end
84
+
85
+ def post_update_hooks_ok
86
+ hook_ok :update
87
+ end
88
+
89
+ def homepage_present
90
+ if @facts.has_key? 'homepage'
91
+ ["#{Tty.green}found#{Tty.reset}", nil]
92
+ else
93
+ ["#{Tty.red}missing#{Tty.reset}", ['adding a homepage improves the credibility', 'of your package']]
94
+ end
95
+ end
96
+
97
+ def version_present
98
+ if @facts.has_key? 'version'
99
+ ["#{Tty.green}found#{Tty.reset}", nil]
100
+ else
101
+ ["#{Tty.red}missing#{Tty.reset}", ['adding a version to the manifest improves', 'a future update experince']]
102
+ end
103
+ end
104
+
105
+ def config_files_present
106
+ if @facts.config_files.empty?
107
+ ["#{Tty.red}missing#{Tty.reset}", ['Add config_files see manual for more information']]
108
+ elsif @facts.config_files.any?{|f| !File.exist? f}
109
+ ["#{Tty.red}error#{Tty.reset}", @facts.config_files.select{|f| !File.exist? f}.map{|f| "#{f.to_s.split("/").last} is missing from this package"}]
110
+ else
111
+ ["#{Tty.green}found#{Tty.reset}", nil]
112
+ end
113
+ end
114
+
115
+ def validate
116
+ self.class.validations.each do |validation|
117
+ errors << [validation[0], *send(validation[1])]
118
+ end
119
+ end
120
+
121
+ def has_errors?
122
+ errors.any?{|e| !e[2].nil? }
123
+ end
124
+ end
@@ -0,0 +1,62 @@
1
+ module LaceArgvExtension
2
+ def named
3
+ @named ||= reject{|arg| arg[0..0] == '-'}
4
+ end
5
+
6
+ def options_only
7
+ select {|arg| arg[0..0] == '-'}
8
+ end
9
+
10
+ # self documenting perhaps?
11
+ def include? arg
12
+ @n=index arg
13
+ end
14
+
15
+ def next
16
+ at @n+1 or raise UsageError
17
+ end
18
+
19
+ def value arg
20
+ arg = find {|o| o =~ /--#{arg}=(.+)/}
21
+ $1 if arg
22
+ end
23
+
24
+ def verbose?
25
+ flag? '--verbose' or !ENV['VERBOSE'].nil?
26
+ end
27
+
28
+ def debug?
29
+ flag? '--debug'
30
+ end
31
+
32
+ def nohooks?
33
+ flag? '--no-hooks'
34
+ end
35
+
36
+ def interactive?
37
+ flag? '--interactive'
38
+ end
39
+
40
+ def flag? flag
41
+ options_only.any? do |arg|
42
+ arg == flag || arg[1..1] != '-' && arg.include?(flag[2..2])
43
+ end
44
+ end
45
+
46
+ # eg. `foo -ns -i --bar` has three switches, n, s and i
47
+ def switch? switch_character
48
+ return false if switch_character.length > 1
49
+ options_only.any? do |arg|
50
+ arg[1..1] != '-' && arg.include?(switch_character)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def downcased_unique_named
57
+ # Only lowercase names, not paths or URLs
58
+ @downcased_unique_named ||= named.map do |arg|
59
+ arg.include?("/") ? arg : arg.downcase
60
+ end.uniq
61
+ end
62
+ end
@@ -0,0 +1,247 @@
1
+ require 'pathname'
2
+
3
+ # we enhance pathname to make our code more readable
4
+ class Pathname
5
+
6
+ def cp dst
7
+ if file?
8
+ FileUtils.cp to_s, dst
9
+ else
10
+ FileUtils.cp_r to_s, dst
11
+ end
12
+ return dst
13
+ end
14
+
15
+ # extended to support common double extensions
16
+ alias extname_old extname
17
+ def extname(path=to_s)
18
+ BOTTLE_EXTNAME_RX.match(path)
19
+ return $1 if $1
20
+ /(\.(tar|cpio|pax)\.(gz|bz2|xz|Z))$/.match(path)
21
+ return $1 if $1
22
+ return File.extname(path)
23
+ end
24
+
25
+ # for filetypes we support, basename without extension
26
+ def stem
27
+ File.basename((path = to_s), extname(path))
28
+ end
29
+
30
+ # I don't trust the children.length == 0 check particularly, not to mention
31
+ # it is slow to enumerate the whole directory just to see if it is empty,
32
+ # instead rely on good ol' libc and the filesystem
33
+ def rmdir_if_possible
34
+ rmdir
35
+ true
36
+ rescue Errno::ENOTEMPTY
37
+ if (ds_store = self+'.DS_Store').exist? && children.length == 1
38
+ ds_store.unlink
39
+ retry
40
+ else
41
+ false
42
+ end
43
+ rescue Errno::EACCES, Errno::ENOENT
44
+ false
45
+ end
46
+
47
+ def chmod_R perms
48
+ require 'fileutils'
49
+ FileUtils.chmod_R perms, to_s
50
+ end
51
+
52
+ def abv
53
+ out=''
54
+ n=`find #{to_s} -type f ! -name .DS_Store | wc -l`.to_i
55
+ out<<"#{n} files, " if n > 1
56
+ out<<`/usr/bin/du -hs #{to_s} | cut -d"\t" -f1`.strip
57
+ end
58
+
59
+ def compression_type
60
+ # Don't treat jars or wars as compressed
61
+ return nil if self.extname == '.jar'
62
+ return nil if self.extname == '.war'
63
+
64
+ # OS X installer package
65
+ return :pkg if self.extname == '.pkg'
66
+
67
+ # If the filename ends with .gz not preceded by .tar
68
+ # then we want to gunzip but not tar
69
+ return :gzip_only if self.extname == '.gz'
70
+
71
+ # Get enough of the file to detect common file types
72
+ # POSIX tar magic has a 257 byte offset
73
+ # magic numbers stolen from /usr/share/file/magic/
74
+ case open('rb') { |f| f.read(262) }
75
+ when /^PK\003\004/n then :zip
76
+ when /^\037\213/n then :gzip
77
+ when /^BZh/n then :bzip2
78
+ when /^\037\235/n then :compress
79
+ when /^.{257}ustar/n then :tar
80
+ when /^\xFD7zXZ\x00/n then :xz
81
+ when /^Rar!/n then :rar
82
+ when /^7z\xBC\xAF\x27\x1C/n then :p7zip
83
+ else
84
+ # This code so that bad-tarballs and zips produce good error messages
85
+ # when they don't unarchive properly.
86
+ case extname
87
+ when ".tar.gz", ".tgz", ".tar.bz2", ".tbz" then :tar
88
+ when ".zip" then :zip
89
+ end
90
+ end
91
+ end
92
+
93
+ def text_executable?
94
+ %r[^#!\s*\S+] === open('r') { |f| f.read(1024) }
95
+ end
96
+
97
+ def incremental_hash(hasher)
98
+ incr_hash = hasher.new
99
+ buf = ""
100
+ open('rb') { |f| incr_hash << buf while f.read(1024, buf) }
101
+ incr_hash.hexdigest
102
+ end
103
+
104
+ def sha1
105
+ require 'digest/sha1'
106
+ incremental_hash(Digest::SHA1)
107
+ end
108
+
109
+ def sha256
110
+ require 'digest/sha2'
111
+ incremental_hash(Digest::SHA2)
112
+ end
113
+
114
+ if '1.9' <= RUBY_VERSION
115
+ alias_method :to_str, :to_s
116
+ end
117
+
118
+ def cd
119
+ Dir.chdir(self){ yield }
120
+ end
121
+
122
+ def subdirs
123
+ children.select{ |child| child.directory? }
124
+ end
125
+
126
+ def resolved_path
127
+ self.symlink? ? dirname+readlink : self
128
+ end
129
+
130
+ def resolved_path_exists?
131
+ link = readlink
132
+ rescue ArgumentError
133
+ # The link target contains NUL bytes
134
+ false
135
+ else
136
+ (dirname+link).exist?
137
+ end
138
+
139
+ # perhaps confusingly, this Pathname object becomes the symlink pointing to
140
+ # the src paramter.
141
+ def make_relative_symlink src
142
+ src = Pathname.new(src) unless src.kind_of? Pathname
143
+
144
+ self.dirname.mkpath
145
+ Dir.chdir self.dirname do
146
+ # NOTE only system ln -s will create RELATIVE symlinks
147
+ quiet_system 'ln', '-s', src.relative_path_from(self.dirname), self.basename
148
+ if not $?.success?
149
+ if self.exist?
150
+ raise <<-EOS.undent
151
+ Could not symlink file: #{src.expand_path}
152
+ Target #{self} already exists. You may need to delete it.
153
+ To force the link and overwrite all other conflicting files, do:
154
+ brew link --overwrite formula_name
155
+
156
+ To list all files that would be deleted:
157
+ brew link --overwrite --dry-run formula_name
158
+ EOS
159
+ # #exist? will return false for symlinks whose target doesn't exist
160
+ elsif self.symlink?
161
+ raise <<-EOS.undent
162
+ Could not symlink file: #{src.expand_path}
163
+ Target #{self} already exists as a symlink to #{readlink}.
164
+ If this file is from another formula, you may need to
165
+ `brew unlink` it. Otherwise, you may want to delete it.
166
+ To force the link and overwrite all other conflicting files, do:
167
+ brew link --overwrite formula_name
168
+
169
+ To list all files that would be deleted:
170
+ brew link --overwrite --dry-run formula_name
171
+ EOS
172
+ elsif !dirname.writable_real?
173
+ raise <<-EOS.undent
174
+ Could not symlink file: #{src.expand_path}
175
+ #{dirname} is not writable. You should change its permissions.
176
+ EOS
177
+ else
178
+ raise <<-EOS.undent
179
+ Could not symlink file: #{src.expand_path}
180
+ #{self} may already exist.
181
+ #{dirname} may not be writable.
182
+ EOS
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ def / that
189
+ join that.to_s
190
+ end
191
+
192
+ def ensure_writable
193
+ saved_perms = nil
194
+ unless writable_real?
195
+ saved_perms = stat.mode
196
+ chmod 0644
197
+ end
198
+ yield
199
+ ensure
200
+ chmod saved_perms if saved_perms
201
+ end
202
+
203
+ # Writes an exec script in this folder for each target pathname
204
+ def write_exec_script *targets
205
+ targets.flatten!
206
+ if targets.empty?
207
+ opoo "tried to write exec scripts to #{self} for an empty list of targets"
208
+ end
209
+ targets.each do |target|
210
+ target = Pathname.new(target) # allow pathnames or strings
211
+ (self+target.basename()).write <<-EOS.undent
212
+ #!/bin/bash
213
+ exec "#{target}" "$@"
214
+ EOS
215
+ end
216
+ end
217
+
218
+ # We redefine these private methods in order to add the /o modifier to
219
+ # the Regexp literals, which forces string interpolation to happen only
220
+ # once instead of each time the method is called. This is fixed in 1.9+.
221
+ if RUBY_VERSION <= "1.8.7"
222
+ alias_method :old_chop_basename, :chop_basename
223
+ def chop_basename(path)
224
+ base = File.basename(path)
225
+ if /\A#{Pathname::SEPARATOR_PAT}?\z/o =~ base
226
+ return nil
227
+ else
228
+ return path[0, path.rindex(base)], base
229
+ end
230
+ end
231
+ private :chop_basename
232
+
233
+ alias_method :old_prepend_prefix, :prepend_prefix
234
+ def prepend_prefix(prefix, relpath)
235
+ if relpath.empty?
236
+ File.dirname(prefix)
237
+ elsif /#{SEPARATOR_PAT}/o =~ prefix
238
+ prefix = File.dirname(prefix)
239
+ prefix = File.join(prefix, "") if File.basename(prefix + 'a') != 'a'
240
+ prefix + relpath
241
+ else
242
+ prefix + relpath
243
+ end
244
+ end
245
+ private :prepend_prefix
246
+ end
247
+ end
@@ -0,0 +1,168 @@
1
+ require "fileutils"
2
+
3
+ class AbstractDownloadStrategy
4
+ attr_reader :name, :resource, :target_folder
5
+
6
+ def initialize uri
7
+ @uri = uri
8
+ @target_folder = LACE_PKGS_FOLDER/name
9
+ end
10
+
11
+ # All download strategies are expected to implement these methods
12
+ def fetch; end
13
+ def stage; end
14
+ def name
15
+ ARGV.value "name"
16
+ end
17
+ end
18
+
19
+
20
+ class LocalFileStrategy < AbstractDownloadStrategy
21
+ def fetch
22
+ ohai "Fetching #@uri into #@target_folder"
23
+ FileUtils.cp_r @uri, @target_folder
24
+ @target_folder
25
+ end
26
+
27
+ def name
28
+ super || File.basename(@uri)
29
+ end
30
+ end
31
+
32
+
33
+ module GitCommands
34
+ def update_repo
35
+ safe_system 'git', 'fetch', 'origin'
36
+ end
37
+
38
+ def reset
39
+ safe_system 'git', "reset" , "--hard", "origin/HEAD"
40
+ end
41
+
42
+ def git_dir
43
+ @target_folder.join(".git")
44
+ end
45
+
46
+ def repo_valid?
47
+ quiet_system "git", "--git-dir", git_dir, "status", "-s"
48
+ end
49
+
50
+ def submodules?
51
+ @target_folder.join(".gitmodules").exist?
52
+ end
53
+
54
+ def clone_args
55
+ args = %w{clone}
56
+ args << @uri << @target_folder
57
+ end
58
+
59
+ def clone_repo
60
+ safe_system 'git', *clone_args
61
+ @target_folder.cd { update_submodules } if submodules?
62
+ end
63
+
64
+ def update_submodules
65
+ safe_system 'git', 'submodule', 'update', '--init'
66
+ end
67
+ end
68
+
69
+
70
+ class GitUpdateStrategy
71
+ include GitCommands
72
+
73
+ def initialize name
74
+ @target_folder = LACE_PKGS_FOLDER/name
75
+ end
76
+
77
+ def update
78
+ if repo_valid?
79
+ puts "Updating #@target_folder"
80
+ @target_folder.cd do
81
+ update_repo
82
+ reset
83
+ update_submodules if submodules?
84
+ end
85
+ else
86
+ puts "Removing invalid .git repo"
87
+ FileUtils.rm_rf @target_folder
88
+ clone_repo
89
+ end
90
+ end
91
+ end
92
+
93
+
94
+ class GitDownloadStrategy < AbstractDownloadStrategy
95
+ include GitCommands
96
+
97
+ def fetch
98
+ ohai "Cloning #@uri"
99
+
100
+ if @target_folder.exist? && repo_valid?
101
+ puts "Updating #@target_folder"
102
+ @target_folder.cd do
103
+ update_repo
104
+ reset
105
+ update_submodules if submodules?
106
+ end
107
+ elsif @target_folder.exist?
108
+ puts "Removing invalid .git repo"
109
+ FileUtils.rm_rf @target_folder
110
+ clone_repo
111
+ else
112
+ clone_repo
113
+ end
114
+ @target_folder
115
+ end
116
+
117
+ def name
118
+ if super
119
+ super
120
+ elsif @uri.include? "github.com"
121
+ @uri.split("/")[-2]
122
+ elsif File.directory? @uri
123
+ File.basename(@uri)
124
+ else
125
+ raise "Cannot determine a proper name with #@uri"
126
+ end
127
+ end
128
+
129
+ end
130
+
131
+
132
+ class DownloadStrategyDetector
133
+ def self.detect(uri, strategy=nil)
134
+ if strategy.nil?
135
+ detect_from_uri(uri)
136
+ elsif Symbol === strategy
137
+ detect_from_symbol(strategy)
138
+ else
139
+ raise TypeError,
140
+ "Unknown download strategy specification #{strategy.inspect}"
141
+ end
142
+ end
143
+
144
+ def self.detect_from_uri(uri)
145
+ if File.directory?(uri) && !File.directory?(uri+"/.git")
146
+ return LocalFileStrategy
147
+ elsif File.directory?(uri+"/.git")
148
+ return GitDownloadStrategy
149
+ end
150
+
151
+ case uri
152
+ when %r[^git://] then GitDownloadStrategy
153
+ when %r[^https?://.+\.git$] then GitDownloadStrategy
154
+ # else CurlDownloadStrategy
155
+ else
156
+ raise "Cannot determine download startegy from #{uri}"
157
+ end
158
+ end
159
+
160
+ def self.detect_from_symbol(symbol)
161
+ case symbol
162
+ when :git then GitDownloadStrategy
163
+ when :local_file then LocalFileStrategy
164
+ else
165
+ raise "Unknown download strategy #{strategy} was requested."
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,26 @@
1
+ class UsageError < RuntimeError; end
2
+ class ResourceNotSpecified < ArgumentError; end
3
+ class ErrorDuringExecution < RuntimeError; end
4
+
5
+ class OnlyGitReposCanBeUpdatedError < RuntimeError
6
+ def initialize
7
+ super "Only kits installed via git can be updated"
8
+ end
9
+ end
10
+
11
+ class AlreadyActiveError < RuntimeError
12
+ def initialize
13
+ super "Cannot activate an already active package, please deactivate first"
14
+ end
15
+ end
16
+
17
+ class NonActiveFlavorError < RuntimeError
18
+ def initialize
19
+ super "It looks like the flavor you tried to deactivate is not active after all"
20
+ end
21
+ end
22
+
23
+ FlavorArgumentMsg = <<-EOS
24
+ Sorry, this command needs a flavor argument you can choose from the following:
25
+ - %s
26
+ EOS
@@ -0,0 +1,205 @@
1
+ require 'yaml'
2
+ require 'ostruct'
3
+ require 'set'
4
+
5
+ require 'lace/download_strategy'
6
+ require 'lace/exceptions'
7
+
8
+ class PackageUtils
9
+ def self.is_package_any_flavor_active name
10
+ @path = LACE_PKGS_FOLDER/name
11
+ facts = Facts.new @path
12
+ facts.flavors.any?{|f| Package.new(@name, f).is_active?}
13
+ end
14
+
15
+ def self.fetch uri, argv
16
+ downloader = DownloadStrategyDetector.detect(uri).new(uri)
17
+ if downloader.target_folder.exist?
18
+ raise "Package already installed"
19
+ end
20
+ downloader.fetch
21
+ end
22
+
23
+ def self.remove package_name, argv
24
+ ohai "Removing"
25
+ package = Package.new package_name, false
26
+ unless package.is_active?
27
+ FileUtils.rm_rf package.path
28
+ ohai "Successfully removed"
29
+ else
30
+ ofail "Cannot remove active kit, deactivate first"
31
+ end
32
+ end
33
+
34
+ def self.install uri, argv
35
+ downloader = DownloadStrategyDetector.detect(uri).new(uri)
36
+ if downloader.target_folder.exist?
37
+ raise "Package already installed"
38
+ end
39
+ downloader.fetch
40
+ package = Package.new downloader.name, ARGV.first
41
+ package.activate!
42
+ package.after_install
43
+ end
44
+
45
+ def self.deactivate package_name, argv
46
+ package = Package.new package_name, ARGV.shift
47
+ raise NonActiveFlavorError.new unless package.is_active?
48
+ package.deactivate!
49
+ end
50
+
51
+ def self.activate package_name, argv
52
+ package = Package.new package_name, ARGV.shift
53
+ raise AlreadyActiveError.new if Package.new(package_name, false).is_active?
54
+ package.activate!
55
+ end
56
+
57
+ def self.update package_name, argv
58
+ package = Package.new package_name, false
59
+ raise OnlyGitReposCanBeUpdatedError.new unless package.is_git_repo?
60
+ updater = GitUpdateStrategy.new package_name
61
+ package.deactivate!
62
+ updater.update
63
+ package.read_facts!
64
+ package.activate!
65
+ package.after_update
66
+ end
67
+ end
68
+
69
+ class Facts
70
+ attr_reader :facts_file
71
+ def initialize location
72
+ @location = Pathname.new(location)
73
+ @facts_file = @location/".lace.yml"
74
+ raise RuntimeError.new "No package file found in #@location" unless @facts_file.exist?
75
+ @facts = YAML.load @facts_file.read
76
+ @_facts = YAML.load @facts_file.read
77
+ end
78
+
79
+ def config_files
80
+ if @_facts.nil? or @facts["config_files"].nil?
81
+ []
82
+ else
83
+ @facts["config_files"].flatten.map do |file|
84
+ @location + file
85
+ end
86
+ end
87
+ end
88
+
89
+ def has_flavors?
90
+ @_facts && !@_facts["flavors"].nil?
91
+ end
92
+
93
+ def has_key? key
94
+ @_facts && @_facts.has_key?(key)
95
+ end
96
+
97
+ def version
98
+ @_facts["version"] if @_facts.key? "version"
99
+ end
100
+
101
+ def flavors
102
+ if @_facts && @_facts.key?("flavors")
103
+ @_facts["flavors"].keys
104
+ else
105
+ []
106
+ end
107
+ end
108
+
109
+ def flavor! which_flavor
110
+ raise RuntimeError.new "Flavor '#{which_flavor}' does not exist -> #{flavors.join(', ')} - use: lace <command> <kit-uri> <flavor>" unless flavors.include? which_flavor
111
+ @facts = @_facts["flavors"][which_flavor]
112
+ end
113
+
114
+ def unflavor!
115
+ @facts = @_facts
116
+ end
117
+
118
+ def post hook_point
119
+ if @_facts.nil? or !@facts.key? "post"
120
+ []
121
+ else
122
+ post_hook = @facts["post"]
123
+ (post_hook[hook_point.to_s] || []).flatten
124
+ end
125
+ end
126
+ end
127
+
128
+ class Package
129
+ include GitCommands
130
+ attr_reader :name, :facts, :path
131
+
132
+ def after_install
133
+ return if ARGV.nohooks?
134
+ @path.cd do
135
+ ENV["CURRENT_DOTTY"] = @path
136
+ facts.post(:install).each do |cmd|
137
+ safe_system cmd
138
+ end
139
+ end
140
+ end
141
+
142
+ def after_update
143
+ return if ARGV.nohooks?
144
+ @path.cd do
145
+ ENV["CURRENT_DOTTY"] = @path
146
+ facts.post(:update).each do |cmd|
147
+ system cmd
148
+ end
149
+ end
150
+ end
151
+
152
+ def initialize name, flavor=nil
153
+ require 'cmd/list'
154
+ raise "Package #{name} is not installed" unless Lace.installed_dotties.include? name
155
+ @name = name
156
+ @path = LACE_PKGS_FOLDER/name
157
+ @flavor = flavor
158
+ read_facts!
159
+ end
160
+
161
+ def is_git_repo?
162
+ @target_folder = @path
163
+ repo_valid?
164
+ end
165
+
166
+ def is_active?
167
+ if @facts.has_flavors? && @flavor == false
168
+ @facts.flavors.any?{|f| Package.new(@name, f).is_active?}
169
+ else
170
+ linked_files = Set.new Lace.linked_files.map(&:to_s)
171
+ config_files = Set.new @facts.config_files.map(&:to_s)
172
+ config_files.subset? linked_files
173
+ end
174
+ end
175
+
176
+ def read_facts!
177
+ @facts = Facts.new @path
178
+ if @facts.has_flavors? && @flavor.nil?
179
+ raise RuntimeError.new FlavorArgumentMsg % @facts.flavors.join("\n- ")
180
+ elsif @facts.has_flavors? && @flavor != false
181
+ @facts.flavor! @flavor
182
+ end
183
+ end
184
+
185
+ def deactivate!
186
+ ohai "Deactivating"
187
+ files = @facts.config_files
188
+ home_dir = ENV["HOME"]
189
+ files.each do |file|
190
+ pn = Pathname.new file
191
+ FileUtils.rm_f File.join(home_dir, "." + pn.basename)
192
+ end
193
+ end
194
+
195
+ def activate!
196
+ ohai "Activating"
197
+ files = @facts.config_files
198
+ home_dir = ENV["HOME"]
199
+ files.each do |file|
200
+ # if ends in erb -> generate it
201
+ pn = Pathname.new file
202
+ FileUtils.ln_s file, File.join(home_dir, "." + pn.basename)
203
+ end
204
+ end
205
+ end
data/lib/lace/utils.rb ADDED
@@ -0,0 +1,107 @@
1
+ class Tty
2
+ class << self
3
+ def blue; bold 34; end
4
+ def white; bold 39; end
5
+ def red; underline 31; end
6
+ def yellow; underline 33 ; end
7
+ def reset; escape 0; end
8
+ def em; underline 39; end
9
+ def green; color 92 end
10
+ def gray; bold 30 end
11
+
12
+ def width
13
+ `/usr/bin/tput cols`.strip.to_i
14
+ end
15
+
16
+ def truncate(str)
17
+ str.to_s[0, width - 4]
18
+ end
19
+
20
+ private
21
+
22
+ def color n
23
+ escape "0;#{n}"
24
+ end
25
+ def bold n
26
+ escape "1;#{n}"
27
+ end
28
+ def underline n
29
+ escape "4;#{n}"
30
+ end
31
+ def escape n
32
+ "\033[#{n}m" if $stdout.tty?
33
+ end
34
+ end
35
+ end
36
+
37
+ def ohai title, *sput
38
+ title = Tty.truncate(title) if $stdout.tty? && !ARGV.verbose?
39
+ puts "#{Tty.blue}==>#{Tty.white} #{title}#{Tty.reset}"
40
+ puts sput unless sput.empty?
41
+ end
42
+
43
+ def oh1 title
44
+ title = Tty.truncate(title) if $stdout.tty? && !ARGV.verbose?
45
+ puts "#{Tty.green}==>#{Tty.white} #{title}#{Tty.reset}"
46
+ end
47
+
48
+ def opoo warning
49
+ STDERR.puts "#{Tty.red}Warning#{Tty.reset}: #{warning}"
50
+ end
51
+
52
+ def onoe error
53
+ lines = error.to_s.split("\n")
54
+ STDERR.puts "#{Tty.red}Error#{Tty.reset}: #{lines.shift}"
55
+ STDERR.puts lines unless lines.empty?
56
+ end
57
+
58
+ def ofail error
59
+ onoe error
60
+ Lace.failed = true
61
+ end
62
+
63
+ def odie error
64
+ onoe error
65
+ exit 1
66
+ end
67
+
68
+ def determine_os
69
+ case RUBY_PLATFORM
70
+ when /darwin/ then :mac
71
+ when /linux/ then :linux
72
+ else raise InvalidOSError
73
+ end
74
+ end
75
+
76
+ module Lace extend self
77
+ def system cmd, *args
78
+ puts "#{cmd} #{args*' '}" if ARGV.verbose?
79
+ fork do
80
+ yield if block_given?
81
+ args.collect!{|arg| arg.to_s}
82
+ exec(cmd.to_s, *args) rescue nil
83
+ exit! 1 # never gets here unless exec failed
84
+ end
85
+ Process.wait
86
+ $?.success?
87
+ end
88
+ end
89
+
90
+ # Kernel.system but with exceptions
91
+ def safe_system cmd, *args
92
+ unless Lace.system cmd, *args
93
+ args = args.map{ |arg| arg.to_s.gsub " ", "\\ " } * " "
94
+ raise ErrorDuringExecution, "Failure while executing: #{cmd} #{args}"
95
+ end
96
+ end
97
+
98
+ # prints no output
99
+ def quiet_system cmd, *args
100
+ Lace.system(cmd, *args) do
101
+ # Redirect output streams to `/dev/null` instead of closing as some programs
102
+ # will fail to execute if they can't write to an open stream.
103
+ $stdout.reopen('/dev/null')
104
+ $stderr.reopen('/dev/null')
105
+ end
106
+ end
107
+
@@ -0,0 +1,3 @@
1
+ module Lace
2
+ VERSION = "0.2.1"
3
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lace
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kai Richard Koenig
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-01-04 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: This is a simple/unfinished tool which i use to manage my dotfiles on
15
+ all the different machines
16
+ email: kai@kairichardkoenig.de
17
+ executables:
18
+ - lace
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - lib/cmd/activate.rb
23
+ - lib/cmd/deactivate.rb
24
+ - lib/cmd/fetch.rb
25
+ - lib/cmd/help.rb
26
+ - lib/cmd/inspect.rb
27
+ - lib/cmd/install.rb
28
+ - lib/cmd/list.rb
29
+ - lib/cmd/remove.rb
30
+ - lib/cmd/update.rb
31
+ - lib/cmd/validate.rb
32
+ - lib/extend/ARGV.rb
33
+ - lib/extend/pathname.rb
34
+ - lib/lace/download_strategy.rb
35
+ - lib/lace/exceptions.rb
36
+ - lib/lace/package.rb
37
+ - lib/lace/utils.rb
38
+ - lib/lace/version.rb
39
+ - bin/lace
40
+ homepage: https://github.com/kairichard/lace
41
+ licenses:
42
+ - MIT
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: 1.8.6
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project:
61
+ rubygems_version: 1.8.23
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: Manage your .dotfiles
65
+ test_files: []