bits-installer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ require 'bits/bit_reader'
2
+
3
+ module Bits
4
+ class BitReaderLocal < BitReader
5
+ def initialize(path)
6
+ @path = path
7
+ end
8
+
9
+ def read
10
+ File.new(@path).read
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ module Bits
2
+ class BitReader
3
+ def read
4
+ raise 'not implemented: read'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,48 @@
1
+ require 'optparse'
2
+
3
+ module Bits
4
+ # Baseclass for commands.
5
+ class Command
6
+ attr_reader :ns
7
+
8
+ def setup(opts)
9
+ raise "not implemented: setup"
10
+ end
11
+
12
+ def initialize(ns)
13
+ @ns = ns
14
+ end
15
+ end
16
+
17
+ class << self
18
+ def commands
19
+ @commands ||= {}
20
+ end
21
+
22
+ def define_command(command_id, params={}, &block)
23
+ if commands[command_id]
24
+ raise "Command already defined: #{command_id}"
25
+ end
26
+
27
+ desc = params[:desc] || "(no description)"
28
+
29
+ klass = Class.new(Command) do
30
+ @command_id = command_id
31
+ @name = command_id.to_s.capitalize
32
+ @desc = desc
33
+
34
+ class << self
35
+ attr_reader :command_id, :name, :desc
36
+
37
+ def to_s
38
+ "Bits::#{@name}"
39
+ end
40
+ end
41
+ end
42
+
43
+ klass.class_eval(&block)
44
+
45
+ commands[command_id] = klass
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,39 @@
1
+ require 'bits/spawn'
2
+ require 'json'
3
+
4
+ module Bits
5
+ # Helper functions for writing providers which mainly interacts with external
6
+ # commands.
7
+ module CommandProvider
8
+ module ClassMethods
9
+ def check_command(command, name)
10
+ begin
11
+ exit_code = Bits.spawn command, :stdout => NULL
12
+ rescue Errno::ENOENT
13
+ log.debug "#{name} command not available"
14
+ return false
15
+ end
16
+
17
+ if exit_code != 0 then
18
+ log.debug "#{name} command could not be invoked"
19
+ return false
20
+ end
21
+
22
+ log.debug "#{name} command available"
23
+ true
24
+ end
25
+ end
26
+
27
+ def check_command(*args)
28
+ self.class.check_command(*args)
29
+ end
30
+
31
+ def run(*args)
32
+ self.class.run(*args)
33
+ end
34
+
35
+ def self.included(base)
36
+ base.extend ClassMethods
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,75 @@
1
+ require 'bits/command'
2
+ require 'bits/logging'
3
+
4
+ require 'highline'
5
+
6
+ module Bits
7
+ define_command :install, :desc => 'Install a package' do
8
+ include Bits::Logging
9
+
10
+ def setup(opts)
11
+ ns[:force] = false
12
+ ns[:compiled] = nil
13
+
14
+ opts.banner = "Usage: bits install <bit>"
15
+ opts.on('--[no-]compiled', "Insist on installing an already compiled variant or not") do |v|
16
+ ns[:compiled] = v
17
+ end
18
+
19
+ opts.on('--force', "Insist on installing even if packages already installed") do |v|
20
+ ns[:force] = v
21
+ end
22
+ end
23
+
24
+ def entry(args)
25
+ if args.empty? then
26
+ puts parser.help
27
+ return 1
28
+ end
29
+
30
+ atom = args[0]
31
+
32
+ repository = ns[:repository]
33
+
34
+ parameters = {}
35
+ parameters[:compiled] = ns[:compiled] if ns.has_key? :compiled
36
+
37
+ p = repository.find_package atom, parameters
38
+
39
+ if p.installed? and not ns[:force]
40
+ log.info "Already installed '#{atom}' using provider(s): #{p.providers_s}"
41
+ return 0
42
+ end
43
+
44
+ matching = p.matching_ppps
45
+
46
+ raise "No matching PPP could be found" if matching.empty?
47
+
48
+ ppp = pick_one atom, matching
49
+
50
+ install_ppp atom, ppp
51
+ return 0
52
+ end
53
+
54
+ def pick_one(atom, matching)
55
+ return matching[0] if matching.size == 1
56
+
57
+ hl = HighLine.new $stdin
58
+
59
+ hl.choose do |menu|
60
+ menu.prompt = "Which provider would you like to install '#{atom}' with?"
61
+ matching.each do |match|
62
+ menu.choice(match.provider.provider_id) { match }
63
+ end
64
+ end
65
+ end
66
+
67
+ def install_ppp(atom, ppp)
68
+ provider = ppp.provider
69
+ package = ppp.package
70
+
71
+ log.info "Installing '#{atom}' using provider: #{provider.provider_id}"
72
+ provider.install package
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,35 @@
1
+ require 'bits/command'
2
+ require 'bits/logging'
3
+
4
+ module Bits
5
+ define_command :remove, :desc => "Remove a package" do
6
+ include Bits::Logging
7
+
8
+ def setup(opts)
9
+ opts.banner = "Usage: bits remove <bit>"
10
+ end
11
+
12
+ def entry(args)
13
+ if args.empty? then
14
+ puts parser.help
15
+ return 1
16
+ end
17
+
18
+ atom = args[0]
19
+
20
+ repository = @ns[:repository]
21
+
22
+ p = repository.find_package atom
23
+
24
+ unless p.installed?
25
+ log.info "Package not installed '#{atom}'"
26
+ return 0
27
+ end
28
+
29
+ log.info "Removing '#{atom}' using provider(s): #{p.providers_s}"
30
+
31
+ p.remove
32
+ return 0
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,49 @@
1
+ require 'bits/command'
2
+ require 'bits/logging'
3
+ require 'bits/exceptions'
4
+
5
+ module Bits
6
+ define_command :show, :desc => "Show a package" do
7
+ include Bits::Logging
8
+
9
+ def setup(opts)
10
+ opts.banner = "Usage: bits show <bit>"
11
+ opts.separator ""
12
+ opts.separator "Show information about the specified bit."
13
+ end
14
+
15
+ def entry(args)
16
+ if args.empty? then
17
+ puts parser.help
18
+ return 1
19
+ end
20
+
21
+ atom = args[0]
22
+
23
+ repository = ns[:repository]
24
+
25
+ criteria = {}
26
+ criteria[:compiled] = ns[:compiled] if ns.has_key? :compiled
27
+
28
+ begin
29
+ p = repository.find_package atom, criteria
30
+ rescue MissingBit
31
+ puts "No such atom '#{atom}'"
32
+ return 1
33
+ end
34
+
35
+ puts "PPPs:"
36
+
37
+ p.ppps.each do |ppp|
38
+ puts " #{ppp.provider.provider_id}:"
39
+ puts " Bit: #{ppp.bit.atom}"
40
+ puts " Package Atom: #{ppp.package.atom}"
41
+ puts " Installed: #{ppp.package.installed_s}"
42
+ puts " Candidate: #{ppp.package.candidate_s}"
43
+ puts " Parameters: #{ppp.parameters}"
44
+ end
45
+
46
+ return 0
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,34 @@
1
+ require 'bits/command'
2
+ require 'bits/logging'
3
+ require 'bits/exceptions'
4
+
5
+ require 'fileutils'
6
+
7
+ module Bits
8
+ define_command :sync, :desc => "sync local repository" do
9
+ include Bits::Logging
10
+
11
+ GIT = 'git'
12
+ CLONE_URL = 'https://github.com/udoprog/bits-repo'
13
+
14
+ def setup(opts)
15
+ opts.banner = "Usage: bits sync"
16
+ opts.separator ""
17
+ opts.separator "Sync local repository"
18
+ end
19
+
20
+ def entry(args)
21
+ dir = ns[:local_repository_dir]
22
+
23
+ setup_original dir unless File.directory? dir
24
+
25
+ Dir.chdir(dir) do
26
+ Bits.spawn [GIT, 'pull', 'origin', 'master']
27
+ end
28
+ end
29
+
30
+ def setup_original(dir)
31
+ Bits.spawn [GIT, 'clone', CLONE_URL, dir]
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ module Bits
2
+ class ProviderException < Exception; end
3
+
4
+ # Is raised when a package being requested does not exist.
5
+ class MissingPackage < ProviderException; end
6
+
7
+ # Is raised when a bit does not exist.
8
+ class MissingBit < ProviderException; end
9
+
10
+ # Is raised when a package being requested does not exist.
11
+ class MissingProvidedPackage < ProviderException; end
12
+ end
@@ -0,0 +1,24 @@
1
+ module Bits
2
+ class ExecuteContext
3
+ SUDO = 'sudo'
4
+
5
+ def initialize(user)
6
+ @user = user
7
+ end
8
+
9
+ def spawn(args, params={})
10
+ superuser = params[:superuser] || false
11
+
12
+ if superuser and not @user.superuser?
13
+ Bits.spawn [SUDO] + args, params
14
+ else
15
+ Bits.spawn args, params
16
+ end
17
+ end
18
+
19
+ def run(args, params={})
20
+ params[:ignore_exitcode] = true
21
+ spawn(args, params) == 0
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,204 @@
1
+ require 'bits/logging'
2
+
3
+ require 'rubygems'
4
+ require 'json'
5
+
6
+ module Bits
7
+ module ExternalInterface
8
+ class Interface
9
+ include Bits::Logging
10
+
11
+ attr_reader :capabilities
12
+
13
+ def initialize(id, args, stdin, stdout, pid)
14
+ @id = id
15
+ @args = args
16
+ @stdin = stdin
17
+ @stdout = stdout
18
+ @pid = pid
19
+ @capabilities = []
20
+ end
21
+
22
+ # end the child process by closing stdin.
23
+ def end
24
+ @stdin.close
25
+ Process.wait @pid
26
+ $?
27
+ end
28
+
29
+ def info(atom)
30
+ type, response = request :info, :atom => atom
31
+ end
32
+
33
+ def ping
34
+ begin
35
+ type, response = request :ping
36
+ rescue
37
+ log.debug "problem while pinging interface '#{@id}': #{$!}"
38
+ return nil
39
+ end
40
+
41
+ raise "Expected pong but got #{type}" unless type == :pong
42
+ @capabilities = (response['capabilities'] || []).map(&:to_sym)
43
+ return true
44
+ end
45
+
46
+ def request(type, command={})
47
+ command[:__type__] = type
48
+
49
+ write JSON.dump(command)
50
+ data = read
51
+
52
+ raise "empty response" if data.nil?
53
+
54
+ response = JSON.load(data)
55
+
56
+ response_type = response['__type__'].to_sym
57
+
58
+ if response_type == :error
59
+ error_text = response['text']
60
+ raise "Error in interface: #{error_text}"
61
+ end
62
+
63
+ [response_type, response]
64
+ end
65
+
66
+ def close
67
+ return if @stdin.nil? and @stdout.nil?
68
+ reap_child
69
+ end
70
+
71
+ private
72
+
73
+ def reap_child
74
+ @stdin.close unless @stdin.nil?
75
+ @stdout.close unless @stdout.nil?
76
+
77
+ @stdin = nil
78
+ @stdout = nil
79
+
80
+ # don't hang since write might have reaped it already.
81
+ Process.wait @pid, Process::WNOHANG
82
+ end
83
+
84
+ def write(data)
85
+ return if @stdin.nil?
86
+ @stdin.puts data
87
+ @stdin.flush
88
+ rescue
89
+ reap_child
90
+ raise
91
+ end
92
+
93
+ # read or no-op if it has been closed.
94
+ def read
95
+ return if @stdout.nil?
96
+ @stdout.gets
97
+ rescue
98
+ reap_child
99
+ raise
100
+ end
101
+ end
102
+
103
+ module ClassMethods
104
+ # access global interface cache for class methods.
105
+ def interfaces
106
+ ExternalInterface.interfaces
107
+ end
108
+
109
+ # Spawn an interface that is shared between all users.
110
+ def setup_interface(id, params = {})
111
+ capabilities = params[:capabilities] || []
112
+
113
+ interface = spawn_interface id
114
+
115
+ if interface.nil?
116
+ log.debug "Interface '#{id}' not available"
117
+ return false
118
+ end
119
+
120
+ missing_capabilities = capabilities - interface.capabilities
121
+
122
+ unless missing_capabilities.empty?
123
+ missing_s = missing_capabilities.join ', '
124
+ log.debug "Interface '#{id}' is available, but is missing capabilities: #{missing_s}"
125
+ return false
126
+ end
127
+
128
+ has_s = capabilities.join ', '
129
+ log.debug "Interface '#{id}' is available with capabilities: #{has_s}"
130
+ return true
131
+ end
132
+
133
+ private
134
+
135
+ def spawn_interface(id)
136
+ unless interfaces[id].nil?
137
+ return interfaces[id]
138
+ end
139
+
140
+ libexec_path = path_to_libexec "bits-#{id}"
141
+
142
+ command = [libexec_path]
143
+
144
+ stdout_r, stdout_w = IO.pipe
145
+ stdin_r, stdin_w = IO.pipe
146
+
147
+ pid = fork do
148
+ stdin_w.close
149
+ stdout_r.close
150
+
151
+ $stdin.reopen stdin_r
152
+ $stdout.reopen stdout_w
153
+
154
+ begin
155
+ exec(*command)
156
+ rescue Errno::ENOENT
157
+ $stdout.close
158
+ $stdin.close
159
+ exit ENOENT
160
+ end
161
+
162
+ exit 1
163
+ end
164
+
165
+ stdin_r.close
166
+ stdout_w.close
167
+
168
+ interface = Interface.new(id, command, stdin_w, stdout_r, pid)
169
+
170
+ if not interface.ping
171
+ interface.close
172
+ return nil
173
+ end
174
+
175
+ interfaces[id] = interface
176
+ end
177
+
178
+ def path_to_libexec(name)
179
+ File.join File.dirname(File.expand_path(__FILE__)), File.join('..', 'libexec', name)
180
+ end
181
+ end
182
+
183
+ # global cache for interfaces.
184
+ def self.interfaces
185
+ @interfaces ||= {}
186
+ end
187
+
188
+ # access global interface cache for instances.
189
+ def interfaces
190
+ self.class.interfaces
191
+ end
192
+
193
+ # close all global interfaces
194
+ def self.close_interfaces
195
+ interfaces.each do |id, interface|
196
+ interface.close
197
+ end
198
+ end
199
+
200
+ def self.included(base)
201
+ base.extend ClassMethods
202
+ end
203
+ end
204
+ end