matthewtodd-captain 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.
data/README.rdoc ADDED
@@ -0,0 +1,65 @@
1
+ = Captain
2
+
3
+ Captain builds an Ubuntu installation CD just as you like it.
4
+
5
+ It gathers packages from any number of repositories, preseeds almost all of the installer questions, and runs on any platform with <tt>mkisofs</tt> (including OSX).
6
+
7
+ == Behold
8
+
9
+ With no external configuration, Captain builds an i386 Jaunty CD:
10
+
11
+ $ captain
12
+ ...
13
+ $ ls
14
+ ubuntu-9.04-captain-i386.iso
15
+ $
16
+
17
+ == Install
18
+
19
+ Captain uses <tt>mkisofs</tt> to burn the final image.
20
+
21
+ On OSX, you can get this with
22
+
23
+ port install cdrtools
24
+
25
+ And then you're good to go:
26
+
27
+ gem sources --add http://gems.github.com
28
+ gem install matthewtodd-captain
29
+
30
+ == Configure
31
+
32
+ Captain looks for <tt>config/captain.rb</tt> under the current directory.
33
+
34
+ Here are the defaults:
35
+
36
+ architecture 'i386'
37
+ include_packages ['linux-server', 'language-support-en', 'grub']
38
+ install_packages []
39
+ label 'Ubuntu'
40
+ output_directory '.'
41
+ post_install_commands []
42
+ repositories ['http://us.archive.ubuntu.com/ubuntu jaunty main restricted']
43
+ tasks ['minimal', 'standard']
44
+ tag 'captain'
45
+ version '9.04'
46
+ working_directory temporary_directory
47
+
48
+ There are a few things you'll need to be careful with, for now:
49
+
50
+ +repositories+:: By convention, the installation system and udeb packages will be pulled from the first repository in the list. So you'll want to make sure the first repository is a full-fledged Ubuntu repository.
51
+ +tasks+:: <b>MUST include <tt>'minimal'</tt></b>, else the installer will die with the maddeningly not-at-all-the-problem <tt>"failure trying to run: chroot /target mount -t proc proc /proc"</tt>
52
+ +include_packages+:: MUST include a kernel, some language-support package, and grub; else the installer will complain.
53
+ +version+:: Could be determined automatically from, say, the <tt>Release</tt> file in the first repository. But it isn't yet, so beware of the duplication.
54
+
55
+ See the examples[http://github.com/matthewtodd/captain/tree/master/examples] folder for more.
56
+
57
+ == Preseeding
58
+
59
+ See resources/preseed.seed.erb[http://github.com/matthewtodd/captain/blob/master/resources/preseed.seed.erb]. There may be a few controversial decisions in there that could use some configuring: there's just one monolithic disk partition, no "regular" user created, and no http mirrors included in <tt>/etc/apt/sources.list</tt>.
60
+
61
+ Other than the disk partitioning, I suppose most of these things could be handled post-install, though I'm glad to accept patches / suggestions adding the configuration options you need.
62
+
63
+ == Network Cache
64
+
65
+ Captain caches everything it downloads from the network in <tt>$HOME/.captain</tt>.
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ this_rakefile_uses_shoe = <<END
2
+ ----------------------------------------
3
+ Please install Shoe:
4
+ gem sources --add http://gems.github.com
5
+ gem install matthewtodd-shoe
6
+ ----------------------------------------
7
+ END
8
+
9
+ begin
10
+ gem 'matthewtodd-shoe'
11
+ rescue Gem::LoadError
12
+ abort this_rakefile_uses_shoe
13
+ else
14
+ require 'shoe'
15
+ end
16
+
17
+ Shoe.tie('captain', '0.1.0', 'Builds an Ubuntu installation CD just as you like it.') do |spec|
18
+ spec.requirements = ['mkisofs']
19
+ spec.add_development_dependency 'cucumber'
20
+ end
data/bin/captain ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ require 'captain'
5
+
6
+ Captain::Application.run
@@ -0,0 +1,8 @@
1
+ Feature: Installer Image
2
+ In order to install a custom Ubuntu system
3
+ As a perennial yak-shaver
4
+ I want to assemble my own installer CD
5
+
6
+ Scenario: Default Configuration
7
+ When I run captain
8
+ Then I should be able to launch the resulting image "ubuntu-9.04-captain-i386.iso"
@@ -0,0 +1,17 @@
1
+ When /^I run captain$/ do
2
+ @app.run
3
+ end
4
+
5
+ # Before, I had stubbed out all sorts of stuff with ShamRack. That got to be a
6
+ # lot to maintain. So, now I just make sure I can actually boot up the
7
+ # installation system.
8
+ Then /^I should be able to launch the resulting image "(.+)"$/ do |image_name|
9
+ vmware_directory = Pathname.pwd.join(@app.output_directory)
10
+ config_path = vmware_directory.join('captain.vmx')
11
+ hard_disk_path = vmware_directory.join('captain.vmdk')
12
+ cdrom_iso_path = vmware_directory.join(image_name)
13
+
14
+ create_a_vmware_configuration_file(config_path, hard_disk_path, cdrom_iso_path)
15
+ create_an_empty_hard_disk_image(hard_disk_path)
16
+ launch_vmware(config_path)
17
+ end
@@ -0,0 +1,11 @@
1
+ require 'pathname'
2
+ root = Pathname.new(__FILE__).dirname.parent.parent
3
+
4
+ $LOAD_PATH.unshift(root.join('lib'))
5
+ require 'captain'
6
+
7
+ Before do
8
+ root.join('tmp').rmtree if root.join('tmp').directory?
9
+ @app = Captain::Application.new
10
+ @app.output_directory('tmp')
11
+ end
@@ -0,0 +1,46 @@
1
+ require 'erb'
2
+
3
+ module Helpers
4
+ def append_to_path(directory)
5
+ ENV['PATH'] = "#{ENV['PATH']}:#{directory}" if File.directory?(directory)
6
+ end
7
+
8
+ def create_a_vmware_configuration_file(path, hard_disk_path, cdrom_iso_path)
9
+ template = File.read(__FILE__).split('__END__').last.strip
10
+
11
+ File.open(path, 'w') do |config|
12
+ config.write ERB.new(template).result(binding)
13
+ end
14
+ end
15
+
16
+ def create_an_empty_hard_disk_image(path)
17
+ append_to_path('/Applications/Q.app/Contents/MacOS')
18
+ system("qemu-img create -f vmdk #{path} 2G > /dev/null") || raise('Could not create image.')
19
+ end
20
+
21
+ def launch_vmware(config_path)
22
+ append_to_path('/Library/Application Support/VMware Fusion')
23
+ system("vmrun -T fusion start #{config_path} gui") || raise('VMware Error.')
24
+ end
25
+ end
26
+
27
+ World(Helpers)
28
+
29
+ __END__
30
+ config.version = "8"
31
+ virtualHW.version = "7"
32
+ scsi0.present = "FALSE"
33
+ memsize = "512"
34
+ ide0:0.present = "TRUE"
35
+ ide0:0.fileName = "<%= hard_disk_path %>"
36
+ ide1:0.present = "TRUE"
37
+ ide1:0.fileName = "<%= cdrom_iso_path %>"
38
+ ide1:0.deviceType = "cdrom-image"
39
+ floppy0.present = "FALSE"
40
+ ethernet0.present = "TRUE"
41
+ usb.present = "TRUE"
42
+ sound.present = "FALSE"
43
+ displayName = "Captain - Cucumber"
44
+ guestOS = "ubuntu"
45
+ nvram = "captain.nvram"
46
+ workingDir = "<%= path.dirname %>"
@@ -0,0 +1,111 @@
1
+ require 'tmpdir'
2
+
3
+ module Captain
4
+ class Application
5
+ def self.run
6
+ new.run
7
+ end
8
+
9
+ def initialize
10
+ load_default_configuration
11
+ load_configuration
12
+ end
13
+
14
+ def run
15
+ create_bundle_directory
16
+ create_packages
17
+ create_installer_and_its_supporting_files
18
+ create_boot_loader
19
+ create_ubuntu_symlink
20
+ create_iso_image
21
+ end
22
+
23
+ def method_missing(symbol, *args)
24
+ if args.length > 0
25
+ @configuration[symbol] = args.first
26
+ elsif @configuration.has_key?(symbol)
27
+ @configuration[symbol]
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # This is a convenient way to put arbitrary content on the disk.
36
+ def create_bundle_directory
37
+ FileUtils.cp_r('bundle', working_directory) if File.directory?('bundle')
38
+ end
39
+
40
+ def create_packages
41
+ PackageList.new(repositories, architecture, tasks, include_packages.concat(install_packages)).copy_to(working_directory, self)
42
+ end
43
+
44
+ def create_installer_and_its_supporting_files
45
+ mirror, codename = installer_repository_mirror_and_codename
46
+
47
+ Remote.installer_file(mirror, codename, architecture, 'cdrom', 'vmlinuz' ).copy_to(working_directory, 'install', 'vmlinuz')
48
+ Remote.installer_file(mirror, codename, architecture, 'cdrom', 'initrd.gz').copy_to(working_directory, 'install', 'initrd.gz')
49
+
50
+ Resource.template('preseed.seed.erb', template_binding).copy_to(working_directory, 'install', 'preseed.seed')
51
+ Resource.template('disk_base_components.erb', template_binding).copy_to(working_directory, '.disk', 'base_components')
52
+ Resource.template('disk_base_installable.erb', template_binding).copy_to(working_directory, '.disk', 'base_installable')
53
+ Resource.template('disk_cd_type.erb', template_binding).copy_to(working_directory, '.disk', 'cd_type')
54
+ Resource.template('disk_info.erb', template_binding).copy_to(working_directory, '.disk', 'info')
55
+ Resource.template('disk_udeb_include.erb', template_binding).copy_to(working_directory, '.disk', 'udeb_include')
56
+ end
57
+
58
+ def create_boot_loader
59
+ Resource.file('isolinux.bin').copy_to(working_directory, 'isolinux', 'isolinux.bin')
60
+ Resource.file('isolinux.cfg').copy_to(working_directory, 'isolinux', 'isolinux.cfg')
61
+ end
62
+
63
+ def create_ubuntu_symlink
64
+ Dir.chdir(working_directory) do
65
+ FileUtils.symlink('.', 'ubuntu')
66
+ end
67
+ end
68
+
69
+ def create_iso_image
70
+ Image.new(working_directory).burn(iso_image_path)
71
+ end
72
+
73
+ def installer_repository_mirror_and_codename
74
+ repositories.first.split(' ').slice(0, 2)
75
+ end
76
+
77
+ def iso_image_path
78
+ File.join(output_directory, "#{label}-#{version}-#{tag}-#{architecture}.iso".downcase)
79
+ end
80
+
81
+ def template_binding
82
+ binding
83
+ end
84
+
85
+ def load_default_configuration
86
+ @configuration = Hash.new
87
+
88
+ architecture 'i386'
89
+ include_packages ['linux-server', 'language-support-en', 'grub']
90
+ install_packages []
91
+ label 'Ubuntu'
92
+ output_directory '.'
93
+ post_install_commands []
94
+ repositories ['http://us.archive.ubuntu.com/ubuntu jaunty main restricted']
95
+ tasks ['minimal', 'standard']
96
+ tag 'captain'
97
+ version '9.04'
98
+ working_directory temporary_directory
99
+ end
100
+
101
+ def load_configuration
102
+ instance_eval(File.read('config/captain.rb')) if File.exist?('config/captain.rb')
103
+ end
104
+
105
+ def temporary_directory
106
+ temporary_directory = Dir.mktmpdir('captain')
107
+ at_exit { FileUtils.remove_entry_secure(temporary_directory) }
108
+ temporary_directory
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,28 @@
1
+ require 'pathname'
2
+
3
+ module Captain
4
+ class Image
5
+ def initialize(base_directory)
6
+ @base_directory = base_directory
7
+ end
8
+
9
+ def burn(path)
10
+ path = Pathname.new(path)
11
+ path.parent.mkpath unless path.parent.directory?
12
+
13
+ system('mkisofs',
14
+ '-boot-info-table',
15
+ '-boot-load-size', '4',
16
+ '-cache-inodes',
17
+ '-eltorito-boot', 'isolinux/isolinux.bin',
18
+ '-eltorito-catalog', 'isolinux/boot.cat',
19
+ '-full-iso9660-filenames',
20
+ '-joliet',
21
+ '-no-emul-boot',
22
+ '-output', path,
23
+ '-rational-rock',
24
+ '-volid', 'Name',
25
+ @base_directory) || raise('Error creating iso image.')
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,68 @@
1
+ require 'set'
2
+
3
+ module Captain
4
+ class Package
5
+ attr_reader :codename
6
+ attr_reader :component
7
+ attr_reader :dependencies
8
+ attr_reader :filename
9
+ attr_reader :md5sum
10
+ attr_reader :mirror
11
+ attr_reader :name
12
+
13
+ def initialize(mirror, codename, component, manifest)
14
+ @mirror = mirror
15
+ @codename = codename
16
+ @component = component
17
+ @manifest = manifest
18
+
19
+ @dependencies = Set.new
20
+ @provides = Set.new
21
+ @recommends = Set.new
22
+ @tasks = Set.new
23
+
24
+ @manifest.each_line do |line|
25
+ case line
26
+ when /^Depends:(.*)$/
27
+ @dependencies.merge(parse_list($1))
28
+ when /^Filename:(.*)$/
29
+ @filename = $1.strip
30
+ when /^MD5sum:(.*)$/
31
+ @md5sum = $1.strip
32
+ when /^Package:(.*)$/
33
+ @name = $1.strip
34
+ when /^Provides:(.*)$/
35
+ @provides.merge(parse_list($1))
36
+ when /^Recommends:(.*)$/
37
+ @recommends.merge(parse_list($1))
38
+ when /^Task:(.*)$/
39
+ @tasks.merge(parse_list($1))
40
+ end
41
+ end
42
+ end
43
+
44
+ def copy_to(directory)
45
+ Remote.package_file(mirror, filename, md5sum).copy_to(directory, filename)
46
+ end
47
+
48
+ def copy_manifest_to(stream)
49
+ # Just making sure we don't end up with extra newlines. Postel's law and all that.
50
+ stream.puts(@manifest.strip)
51
+ stream.puts
52
+ end
53
+
54
+ def tasks
55
+ @tasks
56
+ end
57
+
58
+ def name_and_provides
59
+ @name_and_provides ||= @provides.dup.add(@name)
60
+ end
61
+
62
+ private
63
+
64
+ def parse_list(string)
65
+ string.split(/[,|]/).map { |versioned| versioned.strip.split(' ').first }
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,102 @@
1
+ module Captain
2
+ class PackageList
3
+ def initialize(sources, architecture, tasks, selectors)
4
+ @sources = sources
5
+ @architecture = architecture
6
+ @tasks = tasks
7
+ @selectors = selectors
8
+ end
9
+
10
+ def copy_to(directory, configuration)
11
+ each_release { |release| release.copy_to(directory, configuration) }
12
+ end
13
+
14
+ private
15
+
16
+ def each_release
17
+ winnow_down(deb_packages).concat(udeb_packages).group_by { |package| package.codename }.each do |codename, packages|
18
+ yield Release.new(codename, @architecture, packages)
19
+ end
20
+ end
21
+
22
+ def winnow_down(packages)
23
+ selected_by_task, remaining_by_task = select_packages_by_task(packages)
24
+ selected_by_name, remaining_by_name = select_packages_by_name(remaining_by_task)
25
+ selected_by_dependencies = select_packages_by_dependencies(remaining_by_name, selected_by_name)
26
+
27
+ selected_by_task.concat(selected_by_name).concat(selected_by_dependencies)
28
+ end
29
+
30
+ def select_packages_by_task(pool)
31
+ pool.partition { |package| !package.tasks.intersection(@tasks).empty? }
32
+ end
33
+
34
+ def select_packages_by_name(pool, names=@selectors)
35
+ pool.partition { |package| !package.name_and_provides.intersection(names).empty? }
36
+ end
37
+
38
+ def select_packages_by_dependencies(pool, dependent_packages)
39
+ if dependent_packages.empty?
40
+ []
41
+ else
42
+ selected, remaining = select_packages_by_name(pool, dependencies_of(dependent_packages))
43
+ selected + select_packages_by_dependencies(remaining, selected)
44
+ end
45
+ end
46
+
47
+ def dependencies_of(packages)
48
+ # We have todo this goofy inject instead of a simple flatten because the dependencies are returned as Sets.
49
+ packages.map { |package| package.dependencies }.inject { |all, each| all.merge(each) }
50
+ end
51
+
52
+ def deb_packages
53
+ deb_components.map { |component| component.packages }.flatten
54
+ end
55
+
56
+ def deb_components
57
+ @sources.map do |source|
58
+ mirror, codename, *components = source.split(' ')
59
+ components.map do |component|
60
+ ComponentManifest.new(mirror, codename, component, @architecture)
61
+ end
62
+ end.flatten
63
+ end
64
+
65
+ def udeb_packages
66
+ mirror, codename, *components = @sources.first.split(' ')
67
+ ComponentManifest.new(mirror, codename, 'main/debian-installer', @architecture).packages
68
+ end
69
+
70
+ class ComponentManifest
71
+ include Enumerable
72
+
73
+ def initialize(mirror, codename, component, architecture)
74
+ @mirror = mirror
75
+ @codename = codename
76
+ @component = component
77
+ @architecture = architecture
78
+ end
79
+
80
+ def each
81
+ buffer = []
82
+ open_stream.each_line do |line|
83
+ if line == "\n"
84
+ yield Package.new(@mirror, @codename, @component, buffer.join)
85
+ buffer.clear
86
+ else
87
+ buffer.push(line)
88
+ end
89
+ end
90
+ end
91
+
92
+ alias_method :packages, :to_a
93
+
94
+ private
95
+
96
+ def open_stream
97
+ Remote.component_file(@mirror, @codename, @component, @architecture, 'Packages.gz').gunzipped
98
+ end
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,101 @@
1
+ require 'digest'
2
+ require 'pathname'
3
+ require 'zlib'
4
+
5
+ module Captain
6
+ class Release
7
+ attr_reader :architecture
8
+ attr_reader :codename
9
+ attr_reader :components
10
+
11
+ def initialize(codename, architecture, packages)
12
+ @codename = codename
13
+ @architecture = architecture
14
+ @components = organize_into_components(packages)
15
+ @packages = packages.sort_by { |p| p.filename }
16
+ end
17
+
18
+ def copy_to(directory, config)
19
+ directory = Pathname.new(directory)
20
+
21
+ @packages.each { |p| p.copy_to(directory) }
22
+ @components.each { |c| c.copy_to(directory.join('dists', @codename)) }
23
+
24
+ Resource.template('release.erb', binding).copy_to(directory.join('dists', @codename, 'Release'))
25
+ end
26
+
27
+ private
28
+
29
+ def organize_into_components(packages)
30
+ packages.group_by { |package| package.component }.map do |name, packages|
31
+ Component.new(name, @architecture, packages)
32
+ end
33
+ end
34
+
35
+ def deb_components
36
+ @components.reject { |c| c.udeb? }
37
+ end
38
+
39
+ class Component
40
+ attr_reader :name
41
+ attr_reader :files
42
+
43
+ def initialize(name, architecture, packages)
44
+ @name = name
45
+ manifest = manifest(packages)
46
+ @files = []
47
+ @files.push Manifest.new("#{name}/binary-#{architecture}/Release", Resource.template('release_component.erb', binding).contents)
48
+ @files.push Manifest.new("#{name}/binary-#{architecture}/Packages", manifest)
49
+ @files.push Manifest.new("#{name}/binary-#{architecture}/Packages.gz", gzip(manifest))
50
+ end
51
+
52
+ def copy_to(directory)
53
+ files.each { |file| file.copy_to(directory) }
54
+ end
55
+
56
+ def udeb?
57
+ name =~ /debian-installer/
58
+ end
59
+
60
+ private
61
+
62
+ def manifest(packages)
63
+ io = StringIO.new
64
+ packages.each { |package| package.copy_manifest_to(io) }
65
+ io.string
66
+ end
67
+
68
+ def gzip(string)
69
+ io = StringIO.new
70
+ gzip = Zlib::GzipWriter.new(io)
71
+ gzip.write(string)
72
+ gzip.close
73
+ io.string
74
+ end
75
+
76
+ class Manifest
77
+ attr_reader :path
78
+
79
+ def initialize(path, contents)
80
+ @path = path
81
+ @contents = contents
82
+ end
83
+
84
+ def copy_to(directory)
85
+ file = Pathname.new(directory).join(path)
86
+ file.dirname.mkpath
87
+ file.open('w') { |io| io.write(@contents) }
88
+ end
89
+
90
+ def checksum(algorithm)
91
+ Digest(algorithm).hexdigest(@contents)
92
+ end
93
+
94
+ def size
95
+ @contents.length
96
+ end
97
+ end
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,232 @@
1
+ require 'digest'
2
+ require 'open-uri'
3
+ require 'pathname'
4
+ require 'uri'
5
+ require 'zlib'
6
+
7
+ module Captain
8
+ class Remote
9
+ def self.release_file(mirror, codename)
10
+ # TODO verify the Release file with GPG
11
+ new("#{mirror}/dists/#{codename}/Release")
12
+ end
13
+
14
+ def self.component_file(mirror, codename, component, architecture, *rest)
15
+ uri = component_uri(mirror, codename, component, architecture, *rest)
16
+ path = [component, "binary-#{architecture}", *rest].join('/')
17
+ md5sum = release_file(mirror, codename).grep(%r{^ \w{32}\s+\d+\s+#{path}}).first.split(' ').first
18
+ new(uri, Verifier::MD5.new(md5sum))
19
+ end
20
+
21
+ def self.installer_file(mirror, codename, architecture, *rest)
22
+ uri = installer_uri(mirror, codename, architecture, *rest)
23
+ md5sum_uri = installer_uri(mirror, codename, architecture, 'MD5SUMS')
24
+ md5sum = new(md5sum_uri).grep(%r{#{rest.join('/')}}).first.split(' ').first
25
+ new(uri, Verifier::MD5.new(md5sum))
26
+ end
27
+
28
+ def self.package_file(mirror, filename, md5sum)
29
+ new("#{mirror}/#{filename}", Verifier::MD5.new(md5sum))
30
+ end
31
+
32
+ def self.component_uri(mirror, codename, component, architecture, *rest)
33
+ ["#{mirror}/dists/#{codename}/#{component}/binary-#{architecture}", *rest].join('/')
34
+ end
35
+
36
+ def self.installer_uri(mirror, codename, architecture, *rest)
37
+ ["#{mirror}/dists/#{codename}/main/installer-#{architecture}/current/images", *rest].join('/')
38
+ end
39
+
40
+ include Enumerable
41
+
42
+ def initialize(uri, verifier=Verifier::Content.new)
43
+ @uri = URI.parse(uri)
44
+ @verifier = verifier
45
+ @cache = Cache::Persistent.new
46
+ end
47
+
48
+ def copy_to(*paths)
49
+ path = Pathname.new(File.join(paths))
50
+ path.dirname.mkpath
51
+
52
+ open_stream do |stream|
53
+ File.open(path, 'w') do |file|
54
+ Stream.copy(stream, file)
55
+ end
56
+ end
57
+ end
58
+
59
+ def each_line
60
+ open_stream do |stream|
61
+ stream.each_line do |line|
62
+ yield line
63
+ end
64
+ end
65
+ end
66
+ alias_method :each, :each_line
67
+
68
+ def gunzipped
69
+ Adapter::Gunzip.new(self)
70
+ end
71
+
72
+ private
73
+
74
+ def open_stream(retry_count=4)
75
+ @cache.open(@uri) do |cache|
76
+ begin
77
+ @verifier.verify(cache)
78
+ yield(cache)
79
+ rescue
80
+ begin
81
+ @uri.open(ProgressMeter.new(@uri).to_open_uri_hash) do |stream|
82
+ @verifier.verify(stream)
83
+ cache.populate(stream)
84
+ yield(cache)
85
+ end
86
+ rescue Errno::ECONNRESET, OpenURI::HTTPError, SocketError, Timeout::Error, Verifier::Error
87
+ retry_count -= 1
88
+ raise if retry_count.zero?
89
+ puts $!.message
90
+ puts "Trying again... (#{retry_count} more)"
91
+ retry
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ module Verifier
98
+ class Error < RuntimeError
99
+ end
100
+
101
+ class Content
102
+ def verify(stream)
103
+ raise(Verifier::Error.new("No content.")) unless stream.read(1)
104
+ ensure
105
+ stream.rewind
106
+ end
107
+ end
108
+
109
+ class MD5
110
+ def initialize(expected)
111
+ @expected = expected
112
+ raise(Verifier::Error.new("No expected MD5Sum given.")) unless @expected
113
+ end
114
+
115
+ def verify(stream)
116
+ actual = md5sum(stream)
117
+ raise(Verifier::Error.new("MD5Sum mismatch: expected #{@expected} but was #{actual}")) unless @expected == actual
118
+ end
119
+
120
+ private
121
+
122
+ def md5sum(stream)
123
+ digest = Digest::MD5.new
124
+ Stream.copy(stream, digest, :update)
125
+ digest.hexdigest
126
+ end
127
+ end
128
+ end
129
+
130
+ module Cache
131
+ class Persistent
132
+ PATH = Pathname.new(ENV['HOME']).join('.captain')
133
+
134
+ def open(uri)
135
+ # I didn't expect to have to use string concatenation here, but PATH
136
+ # gets confused when uri.path starts with a /.
137
+ path = PATH.join("#{uri.host}#{uri.path}")
138
+ if path.exist?
139
+ path.open('r+') { |cache| yield populatable(cache) }
140
+ else
141
+ path.dirname.mkpath
142
+ path.open('w+') { |cache| yield populatable(cache) }
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def populatable(stream)
149
+ def stream.populate(other)
150
+ Stream.copy(other, self)
151
+ end
152
+ stream
153
+ end
154
+ end
155
+ end
156
+
157
+ class ProgressMeter
158
+ # For these ANSI escape sequences and more, see http://en.wikipedia.org/wiki/ANSI_escape_code
159
+ MOVE_CURSOR_UP_1_LINE = "\e[1A"
160
+ ERASE_ENTIRE_LINE = "\e[2K"
161
+ MOVE_CURSOR_TO_COLUMN_1 = "\e[1G" # columns are 1-based, oddly enough
162
+ OVERWRITE_PREVIOUS_LINE = "#{MOVE_CURSOR_UP_1_LINE}#{ERASE_ENTIRE_LINE}#{MOVE_CURSOR_TO_COLUMN_1}"
163
+
164
+ def initialize(uri)
165
+ puts uri
166
+ puts
167
+ end
168
+
169
+ def to_open_uri_hash
170
+ { :content_length_proc => method(:the_total_size_is), :progress_proc => method(:the_currently_downloaded_size_is) }
171
+ end
172
+
173
+ def the_total_size_is(size)
174
+ @total_size = size
175
+ end
176
+
177
+ def the_currently_downloaded_size_is(size)
178
+ @current = size
179
+ report
180
+ end
181
+
182
+ private
183
+
184
+ # TODO report percent complete
185
+ # TODO report time spent
186
+ # TODO report time remaining
187
+ def report
188
+ puts "#{OVERWRITE_PREVIOUS_LINE} #{@current} of #{@total_size}"
189
+ $stdout.flush
190
+ end
191
+ end
192
+
193
+ class Stream
194
+ def self.copy(from, to, method = :write)
195
+ buffer = ''
196
+ to.truncate(0) if to.respond_to?(:truncate)
197
+ to.send(method, buffer) while from.read(16384, buffer)
198
+ ensure
199
+ from.rewind
200
+ to.rewind if to.respond_to?(:rewind)
201
+ end
202
+ end
203
+
204
+ module Adapter
205
+ class Gunzip
206
+ include Enumerable
207
+
208
+ def initialize(stream)
209
+ @stream = stream
210
+ end
211
+
212
+ def each_line
213
+ open_stream do |stream|
214
+ stream.each_line do |line|
215
+ yield line
216
+ end
217
+ end
218
+ end
219
+ alias_method :each, :each_line
220
+
221
+ private
222
+
223
+ def open_stream
224
+ @stream.send(:open_stream) do |stream|
225
+ yield Zlib::GzipReader.new(stream)
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ end
232
+ end
@@ -0,0 +1,43 @@
1
+ require 'erb'
2
+ require 'pathname'
3
+
4
+ module Captain
5
+ class Resource
6
+ PATH = Pathname.new(File.dirname(__FILE__)).join('..', '..', 'resources')
7
+
8
+ def self.file(name)
9
+ new(name)
10
+ end
11
+
12
+ def self.template(name, binding)
13
+ Template.new(name, binding)
14
+ end
15
+
16
+ def initialize(name)
17
+ @name = name
18
+ end
19
+
20
+ def contents
21
+ PATH.join(@name).read
22
+ end
23
+
24
+ def copy_to(*paths)
25
+ path = File.join(paths)
26
+ FileUtils.mkpath(File.dirname(path))
27
+ File.open(path, 'w') { |f| f.write(contents) }
28
+ end
29
+
30
+ private
31
+
32
+ class Template < Resource
33
+ def initialize(name, binding)
34
+ super(name)
35
+ @binding = binding
36
+ end
37
+
38
+ def contents
39
+ ERB.new(super, nil, '<>').result(@binding)
40
+ end
41
+ end
42
+ end
43
+ end
data/lib/captain.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'captain/application'
2
+ require 'captain/image'
3
+ require 'captain/package'
4
+ require 'captain/package_list'
5
+ require 'captain/release'
6
+ require 'captain/remote'
7
+ require 'captain/resource'
8
+
9
+ module Captain
10
+ end
@@ -0,0 +1 @@
1
+ main
File without changes
@@ -0,0 +1 @@
1
+ full_cd/single
@@ -0,0 +1 @@
1
+ <%= label %> <%= version %> <%= tag.capitalize %>
@@ -0,0 +1,4 @@
1
+ netcfg
2
+ ethdetect
3
+ pcmcia-cs-udeb
4
+ wireless-tools-udeb
Binary file
@@ -0,0 +1,9 @@
1
+ # Don't show the boot: prompt
2
+ prompt 0
3
+
4
+ # Automatically boot the linux kernel after 1 (= 10 10ths of a) second
5
+ timeout 10
6
+
7
+ label linux
8
+ kernel /install/vmlinuz
9
+ append file=/cdrom/install/preseed.seed initrd=/install/initrd.gz quiet locale=en_US console-setup/layoutcode=us netcfg/choose_interface=auto --
@@ -0,0 +1,58 @@
1
+ ### Localization
2
+ d-i debian-installer/locale string en_US
3
+ d-i console-setup/ask_detect boolean false
4
+ d-i console-setup/layoutcode string us
5
+
6
+ ### Network configuration
7
+ d-i netcfg/dhcp_timeout string 10
8
+ d-i netcfg/dhcp_failed note
9
+ d-i netcfg/dhcp_options select Configure network manually
10
+
11
+ ### Mirror settings
12
+ d-i mirror/country string US
13
+
14
+ ### Clock and time zone setup
15
+ d-i clock-setup/utc boolean true
16
+ d-i time/zone string UTC
17
+ d-i clock-setup/ntp boolean false
18
+
19
+ ### Partitioning
20
+ d-i partman-auto/init_automatically_partition select biggest_free
21
+ d-i partman-auto/method string regular
22
+ d-i partman-auto/choose_recipe select atomic
23
+ d-i partman/confirm_write_new_label boolean true
24
+ d-i partman/choose_partition select finish
25
+ d-i partman/confirm boolean true
26
+
27
+ ### Base system installation
28
+
29
+ ### Account setup
30
+ d-i passwd/root-login boolean true
31
+ d-i passwd/make-user boolean false
32
+
33
+ ### Apt setup
34
+ d-i apt-setup/use_mirror boolean false
35
+ d-i apt-setup/services-select multiselect
36
+ d-i debian-installer/allow_unauthenticated string true
37
+
38
+ ### Package selection
39
+ tasksel tasksel/first multiselect standard
40
+ d-i pkgsel/include string <%= install_packages.join(' ') %>
41
+ d-i pkgsel/update-policy select none
42
+
43
+ ### Boot loader installation
44
+ d-i grub-installer/only_debian boolean true
45
+ d-i grub-installer/with_other_os boolean true
46
+
47
+ ### Finishing up the installation
48
+ d-i finish-install/reboot_in_progress note
49
+ d-i cdrom-detect/eject boolean false
50
+
51
+ ### X configuration
52
+
53
+ ### Preseeding other packages
54
+ user-setup-udeb user-setup/encrypted-private boolean false
55
+
56
+ #### Advanced options
57
+ ### Running custom commands during the installation
58
+ d-i preseed/late_command string <%= post_install_commands.join('; ') %>
@@ -0,0 +1,23 @@
1
+ Suite: <%= codename %>
2
+ Codename: <%= codename %>
3
+ Date: <%= Time.now.utc.strftime('%a, %e %b %Y %H:%M:%S UTC') %>
4
+ Architectures: <%= architecture %>
5
+ Components: <%= deb_components.map { |c| c.name }.join(' ') %>
6
+ MD5Sum:
7
+ <% components.each do |component| %>
8
+ <% component.files.each do |file| %>
9
+ <%= file.checksum('MD5') %> <%= file.size %> <%= file.path %>
10
+ <% end %>
11
+ <% end %>
12
+ SHA1:
13
+ <% components.each do |component| %>
14
+ <% component.files.each do |file| %>
15
+ <%= file.checksum('SHA1') %> <%= file.size %> <%= file.path %>
16
+ <% end %>
17
+ <% end %>
18
+ SHA256:
19
+ <% components.each do |component| %>
20
+ <% component.files.each do |file| %>
21
+ <%= file.checksum('SHA256') %> <%= file.size %> <%= file.path %>
22
+ <% end %>
23
+ <% end %>
@@ -0,0 +1,2 @@
1
+ Component: <%= name %>
2
+ Architecture: <%= architecture %>
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: matthewtodd-captain
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Todd
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-30 00:00:00 -07:00
13
+ default_executable: captain
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: matthewtodd-shoe
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: cucumber
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description:
36
+ email: matthew.todd@gmail.com
37
+ executables:
38
+ - captain
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README.rdoc
43
+ files:
44
+ - Rakefile
45
+ - README.rdoc
46
+ - bin/captain
47
+ - features/installer_image.feature
48
+ - features/steps
49
+ - features/steps/captain_steps.rb
50
+ - features/support
51
+ - features/support/env.rb
52
+ - features/support/helpers.rb
53
+ - lib/captain
54
+ - lib/captain/application.rb
55
+ - lib/captain/image.rb
56
+ - lib/captain/package.rb
57
+ - lib/captain/package_list.rb
58
+ - lib/captain/release.rb
59
+ - lib/captain/remote.rb
60
+ - lib/captain/resource.rb
61
+ - lib/captain.rb
62
+ - resources/disk_base_components.erb
63
+ - resources/disk_base_installable.erb
64
+ - resources/disk_cd_type.erb
65
+ - resources/disk_info.erb
66
+ - resources/disk_udeb_include.erb
67
+ - resources/isolinux.bin
68
+ - resources/isolinux.cfg
69
+ - resources/preseed.seed.erb
70
+ - resources/release.erb
71
+ - resources/release_component.erb
72
+ has_rdoc: false
73
+ homepage:
74
+ licenses:
75
+ post_install_message:
76
+ rdoc_options:
77
+ - --main
78
+ - README.rdoc
79
+ - --title
80
+ - captain-0.1.0
81
+ - --inline-source
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: "0"
89
+ version:
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: "0"
95
+ version:
96
+ requirements:
97
+ - mkisofs
98
+ rubyforge_project:
99
+ rubygems_version: 1.3.5
100
+ signing_key:
101
+ specification_version: 3
102
+ summary: Builds an Ubuntu installation CD just as you like it.
103
+ test_files: []
104
+