lace 0.2.1

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