macinbox 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1e49666706dec8dad1c141d5753afe62f2c3a228
4
+ data.tar.gz: 6999cf488fa3110249b0e2e03ce00dc8fd92fa4e
5
+ SHA512:
6
+ metadata.gz: 9897e34975452ab3811e5c7d56b697e33cf8c6d7aa1e6d2d57c924911d948d5d5a897f49dbf92eeb37906ac7046b8c336a68a2379618a44468c52a630ebe3f09
7
+ data.tar.gz: cfe86479f9863fa453807345b8f900bee06ebb0a3f6413ee4dd1b4ae05efc4dcc0518355f735d232271643e18aa2e1bf2dcabb4d08da8ff81afc31037c2d87df
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.vagrant/
3
+ /.yardoc
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in macinbox.gemspec
6
+ gemspec
@@ -0,0 +1,20 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ macinbox (1.0.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ rake (10.5.0)
10
+
11
+ PLATFORMS
12
+ ruby
13
+
14
+ DEPENDENCIES
15
+ bundler (~> 1.16)
16
+ macinbox!
17
+ rake (~> 10.0)
18
+
19
+ BUNDLED WITH
20
+ 1.16.1
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ LICENSE
2
+
3
+ The MIT License
4
+
5
+ Copyright (c) 2018 David Kramer.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
@@ -0,0 +1,156 @@
1
+ # macinbox
2
+
3
+ Puts macOS in a Vagrant VMware Fusion box.
4
+
5
+ <p align=center>
6
+ <img src="https://raw.githubusercontent.com/bacongravy/macinbox/demo/demo.gif">
7
+ <i>Some sequences shortened. Original run time 14.5 minutes.</i>
8
+ </p>
9
+
10
+ ## System Requirements
11
+
12
+ * macOS 10.13 High Sierra host operating system
13
+ * At least 8 GB RAM (16 GB recommended)
14
+ * At least 2 cores (4 recommended)
15
+ * At least 30 GB of available disk space (60 GB recommended)
16
+
17
+ ## Dependencies
18
+
19
+ The following software is required. Versions other than those mentioned may work, but these are the latest versions tested:
20
+
21
+ * VMware Fusion Pro 10.1.1
22
+ * Vagrant 2.0.2
23
+ * Vagrant VMware Fusion Plugin 5.0.4
24
+ * macOS 10.13.3 High Sierra installer application
25
+
26
+ [Get VMware Fusion Pro](http://www.vmware.com/products/fusion.html)
27
+ //
28
+ [Get Vagrant](https://www.vagrantup.com/)
29
+ //
30
+ [Get Vagrant VMware Fusion Plugin](https://www.vagrantup.com/vmware/)
31
+ //
32
+ [Get macOS 10.13 High Sierra installer application](http://appstore.com/mac/macoshighsierra)
33
+
34
+ ## Installation
35
+
36
+ Install the gem:
37
+
38
+ $ sudo gem install macinbox
39
+
40
+ ## Basic Usage
41
+
42
+ Run with `sudo` and no arguments, the `macinbox` tool will create and add a Vagrant VMware box named 'macinbox' which boots fullscreen to the desktop of the 'vagrant' user:
43
+
44
+ $ sudo macinbox
45
+
46
+ Please be patient, as this may take a while. (On a 2.5 GHz MacBookPro11,5 it takes about 11 minutes, 30 seconds.) After the tool completes you can create a new Vagrant environment with the box and start it:
47
+
48
+ $ vagrant init macinbox && vagrant up
49
+
50
+ A few moments after running this command you will see your virtual machine's display appear fullscreen. (Press Command-Control-F to exit fullscreen mode.) After the virtual machine completes booting (approximately 1-2 minutes) you will see the desktop of the 'vagrant' user and can begin using the virtual machine.
51
+
52
+ ## Advanced Usage
53
+
54
+ To see the advanced options, pass the `--help` option:
55
+
56
+ ```
57
+ $ macinbox --help
58
+
59
+ Usage: macinbox [options]
60
+ -n, --name NAME Name of the box (default: macinbox)
61
+ -d, --disk SIZE Size (GB) of the disk (default: 64)
62
+ -m, --memory SIZE Size (MB) of the memory (default: 2048)
63
+ -c, --cpu COUNT Number of virtual cores (default: 2)
64
+ -s, --short NAME Short name of the user (default: vagrant)
65
+ -f, --full NAME Full name of the user (default: Vagrant)
66
+ -p, --password PASSWORD Password of the user (default: vagrant)
67
+ --installer PATH Path to the macOS installer app
68
+ --vmware PATH Path to the VMware Fusion app
69
+ --no-auto-login Disable auto login
70
+ --no-skip-mini-buddy Show the mini buddy on first login
71
+ --no-hidpi Disable HiDPI resolutions
72
+ --no-fullscreen Display the virtual machine GUI in a window
73
+ --no-gui Disable the GUI
74
+ --debug Enable debug mode
75
+ -h, --help
76
+ ```
77
+
78
+ Enabling debug mode causes the intermediate files (disk image, VMDK, and box) to be preserved after the tool exits rather than being cleaned up. WARNING!!! These intermediate files are very large and you can run out of disk space very quickly when using this option.
79
+
80
+ This advanced example creates and adds a box named 'macinbox-large-nogui' with 4 cores, 8 GB or RAM, and a 128 GB disk; turns off auto login; and prevents the VMware GUI from being shown when the VM is started:
81
+
82
+ $ macinbox -n macinbox-large-nogui -c 4 -m 8192 -d 128 --no-auto-login --no-gui
83
+
84
+ ## Retina Display and HiDPI Support
85
+
86
+ By default macinbox will configure the guest OS to have HiDPI resolutions enabled, and configure the virtual machine to use the native display resolution. You can disable this behavior using the --no-hidpi option.
87
+
88
+ ## Implementation Details
89
+
90
+ This tool performs the following actions:
91
+
92
+ 1. Creates a new blank disk image
93
+ 1. Installs macOS
94
+ 1. Installs the VMware tools
95
+ 1. Updates the SystemPolicyConfiguration KextPolicy to allow the VMware tools kernel extension to load automatically
96
+ 1. Adds an .InstallerConfiguration file to automate the Setup Assistant app and create a user account on first boot
97
+ 1. Enables password-less sudo
98
+ 1. Enables sshd
99
+ 1. Adds an rc.installer_cleanup script which waits for the user account to be created on first boot and then installs the default insecure Vagrant SSH key in the user's home directory
100
+ 1. Enables HiDPI resolutions
101
+ 1. Converts the image into a VMDK
102
+ 1. Creates a Vagrant box for the VMware provider using the VMDK
103
+ 1. Adds the box to Vagrant
104
+
105
+
106
+ The box created by this tool includes a built-in Vagrantfile which disables the following default Vagrant behaviors:
107
+
108
+ 1. Checking Vagrant Cloud for new versions of the box
109
+ 1. Forwarding from port 2222 on the host to port 22 (ssh) on the guest
110
+ 1. Sharing the root folder of the Vagrant environment as '/vagrant' on the guest
111
+
112
+ To re-enable the default ssh port forwarding you can add the following line to your environment's Vagrantfile:
113
+
114
+ config.vm.network :forwarded_port, guest: 22, host: 2222, id: "ssh"
115
+
116
+ To re-enable the default synced folder you can add the following line to your environment's Vagrantfile:
117
+
118
+ config.vm.synced_folder ".", "/vagrant"
119
+
120
+ ## Design Philosophy
121
+
122
+ This tool is intended to do everything that needs to be done to a fresh install of macOS before the first boot to turn it into a Vagrant VMware box that boots macOS with a seamless user experience. However, this tool is also intended to the do the least amount of configuration possible. Nothing is done that could instead be deferred to a provisioning step in a Vagrantfile or packer template.
123
+
124
+ ## Acknowledgements
125
+
126
+ This project was inspired by the great work of others:
127
+
128
+ * http://grahamgilbert.com/blog/2013/08/23/creating-an-os-x-base-box-for-vagrant-with-packer/
129
+ * http://heavyindustries.io/blog/2015/07/05/create_osx_vagrant_vmware_box.html
130
+ * https://spin.atomicobject.com/2015/11/17/vagrant-osx/
131
+ * https://github.com/timsutton/osx-vm-templates
132
+ * https://github.com/boxcutter/macos
133
+ * https://github.com/chilcote/vfuse
134
+ * http://www.modtitan.com/2017/10/lazy-vm-building-hacks-with-autodmg-and.html
135
+
136
+ ## Why?
137
+
138
+ This project draws inspiration from an episode of Mr. Robot. In the episode, Elliot is shown quickly booting what appeared to be a virtual machine running a fresh Linux desktop environment, in order to examine the contents of an untrusted CD-ROM. As I watched I thought, "I want to be able to do that kind of thing with macOS!". Surely I'm not the only person who has downloaded untrusted software from the internet, and wished that there was an easy way to evaluate it without putting my primary working environment at risk?
139
+
140
+ This project is a direct successor to my [vagrant-box-macos](https://github.com/bacongravy/vagrant-box-macos) project, which itself was heavily inspired by Tim Sutton's [osx-vm-templates](https://github.com/timsutton/osx-vm-templates) project.
141
+
142
+ With the release of macOS 10.12.4 the prevailing techniques for customizing macOS installs were hampered by a new installer requirement that all packages be signed by Apple. After attempting various techniques to allow `vagrant-box-macos` to support macOS 10.13 High Sierra, I decided a different approach to box creation was needed, and `macinbox` was born.
143
+
144
+ This project exclusively supports creating VMware Fusion boxes because of the Vagrant VMware plugin support for linked clones and Vagrant shared folders, and because, having tried the alternatives, I believe VMware Fusion provides the best macOS virtualization experience.
145
+
146
+ ## Development
147
+
148
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
149
+
150
+ To run `macinbox` directly from the root of the git workspace without installing the gem, run `sudo bundle exec macinbox`.
151
+
152
+ To install this gem onto your local machine, run `sudo bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
153
+
154
+ ## Contributing
155
+
156
+ Bug reports and pull requests are welcome on GitHub at https://github.com/bacongravy/macinbox.
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,3 @@
1
+ Vagrant.configure("2") do |config|
2
+ config.vm.box = "macinbox"
3
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "macinbox"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "macinbox/cli"
4
+
5
+ Macinbox::CLI.run!(ARGV)
@@ -0,0 +1,8 @@
1
+ require "macinbox/actions"
2
+ require "macinbox/cli"
3
+ require "macinbox/collector"
4
+ require "macinbox/error"
5
+ require "macinbox/logger"
6
+ require "macinbox/task"
7
+ require "macinbox/tty"
8
+ require "macinbox/version"
@@ -0,0 +1,4 @@
1
+ require "macinbox/actions/create_box_from_vmdk"
2
+ require "macinbox/actions/create_image_from_installer"
3
+ require "macinbox/actions/create_vmdk_from_image"
4
+ require "macinbox/actions/install_box"
@@ -0,0 +1,144 @@
1
+ require 'fileutils'
2
+ require 'rubygems/package'
3
+
4
+ require 'macinbox/error'
5
+ require 'macinbox/logger'
6
+ require 'macinbox/task'
7
+
8
+ module Macinbox
9
+
10
+ module Actions
11
+
12
+ class CreateBoxFromVMDK
13
+
14
+ def initialize(opts)
15
+ @input_vmdk = opts[:vmdk_path] or raise ArgumentError.new(":vmdk_path not specified")
16
+ @output_path = opts[:box_path] or raise ArgumentError.new(":box_path not specified")
17
+
18
+ @box_name = opts[:box_name] or raise ArgumentError.new(":box_name not specified")
19
+ @cpu_count = opts[:cpu_count] or raise ArgumentError.new(":cpu_count not specified")
20
+ @memory_size = opts[:memory_size] or raise ArgumentError.new(":memory_size not specified")
21
+
22
+ @gui = opts[:gui]
23
+ @fullscreen = opts[:fullscreen]
24
+ @hidpi = opts[:hidpi]
25
+
26
+ @collector = opts[:collector] or raise ArgumentError.new(":collector not specified")
27
+ @debug = opts[:debug]
28
+
29
+ raise Macinbox::Error.new("VMDK not found") unless File.exist? @input_vmdk
30
+ end
31
+
32
+ def run
33
+ @temp_dir = Task.backtick %W[ /usr/bin/mktemp -d -t create_box_from_vmdk ]
34
+ @collector.add_temp_dir @temp_dir
35
+
36
+ Logger.info "Assembling the box contents..." do
37
+
38
+ @box_dir = "#{@temp_dir}/#{@box_name}.box"
39
+
40
+ FileUtils.mkdir @box_dir
41
+
42
+ File.open "#{@box_dir}/#{@box_name}.vmx", 'w' do |file|
43
+
44
+ file.write <<~EOF
45
+ .encoding = "UTF-8"
46
+ config.version = "8"
47
+ virtualHW.version = "14"
48
+ numvcpus = "#{@cpu_count}"
49
+ memsize = "#{@memory_size}"
50
+ sata0.present = "TRUE"
51
+ sata0:0.fileName = "#{@box_name}.vmdk"
52
+ sata0:0.present = "TRUE"
53
+ sata0:1.autodetect = "TRUE"
54
+ sata0:1.deviceType = "cdrom-raw"
55
+ sata0:1.fileName = "auto detect"
56
+ sata0:1.startConnected = "FALSE"
57
+ sata0:1.present = "TRUE"
58
+ ethernet0.connectionType = "nat"
59
+ ethernet0.addressType = "generated"
60
+ ethernet0.virtualDev = "e1000e"
61
+ ethernet0.linkStatePropagation.enable = "TRUE"
62
+ ethernet0.present = "TRUE"
63
+ usb.present = "TRUE"
64
+ usb_xhci.present = "TRUE"
65
+ ehci.present = "TRUE"
66
+ ehci:0.parent = "-1"
67
+ ehci:0.port = "0"
68
+ ehci:0.deviceType = "video"
69
+ ehci:0.present = "TRUE"
70
+ pciBridge0.present = "TRUE"
71
+ pciBridge4.present = "TRUE"
72
+ pciBridge4.virtualDev = "pcieRootPort"
73
+ pciBridge4.functions = "8"
74
+ pciBridge5.present = "TRUE"
75
+ pciBridge5.virtualDev = "pcieRootPort"
76
+ pciBridge5.functions = "8"
77
+ pciBridge6.present = "TRUE"
78
+ pciBridge6.virtualDev = "pcieRootPort"
79
+ pciBridge6.functions = "8"
80
+ pciBridge7.present = "TRUE"
81
+ pciBridge7.virtualDev = "pcieRootPort"
82
+ pciBridge7.functions = "8"
83
+ vmci0.present = "TRUE"
84
+ smc.present = "TRUE"
85
+ hpet0.present = "TRUE"
86
+ ich7m.present = "TRUE"
87
+ usb.vbluetooth.startConnected = "TRUE"
88
+ board-id.reflectHost = "TRUE"
89
+ firmware = "efi"
90
+ displayName = "#{@box_name}"
91
+ guestOS = "darwin17-64"
92
+ nvram = "#{@box_name}.nvram"
93
+ virtualHW.productCompatibility = "hosted"
94
+ keyboardAndMouseProfile = "macProfile"
95
+ powerType.powerOff = "soft"
96
+ powerType.powerOn = "soft"
97
+ powerType.suspend = "soft"
98
+ powerType.reset = "soft"
99
+ tools.syncTime = "TRUE"
100
+ sound.autoDetect = "TRUE"
101
+ sound.virtualDev = "hdaudio"
102
+ sound.fileName = "-1"
103
+ sound.present = "TRUE"
104
+ extendedConfigFile = "#{@box_name}.vmxf"
105
+ floppy0.present = "FALSE"
106
+ mks.enable3d = "FALSE"
107
+ gui.fitGuestUsingNativeDisplayResolution = "#{@hidpi ? "TRUE" : "FALSE"}"
108
+ gui.viewModeAtPowerOn = "#{@fullscreen ? "fullscreen" : "windowed"}"
109
+ EOF
110
+
111
+ end
112
+
113
+ File.write "#{@box_dir}/metadata.json", <<~EOF
114
+ {"provider": "vmware_fusion"}
115
+ EOF
116
+
117
+ File.write "#{@box_dir}/Vagrantfile", <<~EOF
118
+ ENV["VAGRANT_DEFAULT_PROVIDER"] = "vmware_fusion"
119
+ Vagrant.configure(2) do |config|
120
+ config.vm.box_check_update = false
121
+ config.vm.network :forwarded_port, guest: 22, host: 2222, id: "ssh", disabled: true
122
+ config.vm.synced_folder ".", "/vagrant", disabled: true
123
+ config.vm.provider "vmware_fusion" do |v|
124
+ v.gui = #{@gui}
125
+ end
126
+ end
127
+ EOF
128
+
129
+ FileUtils.cp @input_vmdk, "#{@box_dir}/#{@box_name}.vmdk"
130
+
131
+ end
132
+
133
+ Logger.info "Moving the box to the destination..." do
134
+ FileUtils.chown ENV["SUDO_USER"], nil, @box_dir
135
+ FileUtils.mv @box_dir, @output_path
136
+ end
137
+
138
+ end
139
+
140
+ end
141
+
142
+ end
143
+
144
+ end
@@ -0,0 +1,264 @@
1
+ require 'fileutils'
2
+ require 'shellwords'
3
+ require 'io/console'
4
+
5
+ require 'macinbox/error'
6
+ require 'macinbox/logger'
7
+ require 'macinbox/task'
8
+
9
+ module Macinbox
10
+
11
+ module Actions
12
+
13
+ class CreateImageFromInstaller
14
+
15
+ def initialize(opts)
16
+ @installer_app = opts[:installer_path] or raise ArgumentError.new(":installer_path not specified")
17
+ @output_path = opts[:image_path] or raise ArgumentError.new(":image_path not specified")
18
+ @vmware_fusion_app = opts[:vmware_path] or raise ArgumentError.new(":vmware_path not specified")
19
+
20
+ @disk_size = opts[:disk_size] or raise ArgumentError.new(":disk_size not specified")
21
+ @short_name = opts[:short_name] or raise ArgumentError.new(":short_name not specified")
22
+ @full_name = opts[:full_name] or raise ArgumentError.new(":full_name not specified")
23
+ @password = opts[:password] or raise ArgumentError.new(":password not specified")
24
+
25
+ @auto_login = opts[:auto_login]
26
+ @skip_mini_buddy = opts[:skip_mini_buddy]
27
+ @hidpi = opts[:hidpi]
28
+
29
+ @collector = opts[:collector] or raise ArgumentError.new(":collector not specified")
30
+ @debug = opts[:debug]
31
+
32
+ raise Macinbox::Error.new("installer app not found") unless File.exist? @installer_app
33
+ raise Macinbox::Error.new("VMware Fusion app not found") unless File.exist? @vmware_fusion_app
34
+ end
35
+
36
+ def run
37
+ create_temp_dir
38
+ check_macos_versions
39
+ create_scratch_image
40
+ install_macos
41
+ install_vmware_tools
42
+ set_spc_kextpolicy
43
+ automate_user_account_creation
44
+ automate_vagrant_ssh_key_installation
45
+ enable_passwordless_sudo
46
+ enable_sshd
47
+ enable_hidpi
48
+ save_image
49
+ end
50
+
51
+ def create_temp_dir
52
+ @temp_dir = Task.backtick %W[ /usr/bin/mktemp -d -t create_image_from_installer ]
53
+ @collector.add_temp_dir @temp_dir
54
+ end
55
+
56
+ def check_macos_versions
57
+ Logger.info "Checking macOS versions..." do
58
+ @install_info_plist = "#{@installer_app}/Contents/SharedSupport/InstallInfo.plist"
59
+ raise Macinbox::Error.new("InstallInfo.plist not found in installer app bundle") unless File.exist? @install_info_plist
60
+
61
+ installer_os_version = Task.backtick %W[ /usr/libexec/PlistBuddy -c #{'Print :System\ Image\ Info:version'} #{@install_info_plist} ]
62
+ installer_os_version_components = installer_os_version.split(".") rescue [0, 0, 0]
63
+ installer_os_version_major = installer_os_version_components[0]
64
+ installer_os_version_minor = installer_os_version_components[1]
65
+ Logger.info "Installer macOS version detected: #{installer_os_version}" if @debug
66
+
67
+ host_os_version = Task.backtick %W[ sw_vers -productVersion ]
68
+ host_os_version_components = host_os_version.split(".") rescue [0, 0, 0]
69
+ host_os_version_major = host_os_version_components[0]
70
+ host_os_version_minor = host_os_version_components[1]
71
+ Logger.info "Host macOS version detected: #{host_os_version}" if @debug
72
+
73
+ if installer_os_version_major != host_os_version_major || installer_os_version_minor != host_os_version_minor
74
+ raise Macinbox::Error.new("host OS version (#{host_os_version}) and installer OS version (#{installer_os_version}) do not match")
75
+ end
76
+ end
77
+ end
78
+
79
+ def create_scratch_image
80
+ Logger.info "Creating and attaching a new blank disk image..." do
81
+ @collector.on_cleanup do
82
+ %x( hdiutil detach -quiet -force #{@scratch_mountpoint.shellescape} > /dev/null 2>&1 ) if @scratch_mountpoint
83
+ end
84
+ @scratch_mountpoint = "#{@temp_dir}/scratch_mountpoint"
85
+ @scratch_image = "#{@temp_dir}/scratch.sparseimage"
86
+ FileUtils.mkdir @scratch_mountpoint
87
+ quiet_flag = @debug ? [] : %W[ -quiet ]
88
+ Task.run %W[ hdiutil create -size #{@disk_size}g -type SPARSE -fs HFS+J -volname #{"Macintosh HD"} -uid 0 -gid 80 -mode 1775 #{@scratch_image} ] + quiet_flag
89
+ Task.run %W[ hdiutil attach #{@scratch_image} -mountpoint #{@scratch_mountpoint} -nobrowse -owners on ] + quiet_flag
90
+ end
91
+ end
92
+
93
+ def install_macos
94
+ Logger.info "Installing macOS..." do
95
+ activity = Logger.prefix + "installer"
96
+ cmd = %W[ installer -verboseR -dumplog -pkg #{@install_info_plist} -target #{@scratch_mountpoint} ]
97
+ opts = @debug ? {} : { :err => [:child, :out] }
98
+ Task.run_with_progress activity, cmd, opts do |line|
99
+ /^installer:%(.*)$/.match(line)[1].to_f rescue nil
100
+ end
101
+ end
102
+ end
103
+
104
+ def install_vmware_tools
105
+ Logger.info "Installing the VMware Tools..." do
106
+
107
+ @collector.on_cleanup do
108
+ %x( hdiutil detach -quiet -force #{@tools_mountpoint.shellescape} > /dev/null 2>&1 ) if @tools_mountpoint
109
+ end
110
+
111
+ @tools_mountpoint = "#{@temp_dir}/tools_mountpoint"
112
+ FileUtils.mkdir @tools_mountpoint
113
+
114
+ tools_image = "#{@vmware_fusion_app}/Contents/Library/isoimages/darwin.iso"
115
+ tools_package = "#{@tools_mountpoint}/Install VMware Tools.app/Contents/Resources/VMware Tools.pkg"
116
+ tools_package_dir = "#{@temp_dir}/tools_package"
117
+
118
+ quiet_flag = @debug ? [] : %W[ -quiet ]
119
+
120
+ Task.run %W[ hdiutil attach #{tools_image} -mountpoint #{@tools_mountpoint} -nobrowse ] + quiet_flag
121
+ Task.run %W[ pkgutil --expand #{tools_package} #{tools_package_dir} ]
122
+ Task.run %W[ ditto -x -z #{tools_package_dir}/files.pkg/Payload #{@scratch_mountpoint} ]
123
+
124
+ scratch_vmhgfs_filesystem_resources = "#{@scratch_mountpoint}/Library/Filesystems/vmhgfs.fs/Contents/Resources"
125
+
126
+ FileUtils.mkdir_p scratch_vmhgfs_filesystem_resources
127
+ FileUtils.ln_s "/Library/Application Support/VMware Tools/mount_vmhgfs", "#{scratch_vmhgfs_filesystem_resources}/"
128
+ end
129
+ end
130
+
131
+ def set_spc_kextpolicy
132
+ Logger.info "Setting the KextPolicy to allow loading the VMware kernel extensions..." do
133
+ scratch_spc_kextpolicy = "#{@scratch_mountpoint}/private/var/db/SystemPolicyConfiguration/KextPolicy"
134
+ Task.run_with_input ["sqlite3", scratch_spc_kextpolicy] do |pipe|
135
+ pipe.write <<~EOF
136
+ PRAGMA foreign_keys=OFF;
137
+ BEGIN TRANSACTION;
138
+ CREATE TABLE kext_load_history_v3 ( path TEXT PRIMARY KEY, team_id TEXT, bundle_id TEXT, boot_uuid TEXT, created_at TEXT, last_seen TEXT, flags INTEGER );
139
+ CREATE TABLE kext_policy ( team_id TEXT, bundle_id TEXT, allowed BOOLEAN, developer_name TEXT, flags INTEGER, PRIMARY KEY (team_id, bundle_id) );
140
+ INSERT INTO kext_policy VALUES('EG7KH642X6','com.vmware.kext.VMwareGfx',1,'VMware, Inc.',1);
141
+ INSERT INTO kext_policy VALUES('EG7KH642X6','com.vmware.kext.vmmemctl',1,'VMware, Inc.',1);
142
+ INSERT INTO kext_policy VALUES('EG7KH642X6','com.vmware.kext.vmhgfs',1,'VMware, Inc.',1);
143
+ CREATE TABLE kext_policy_mdm ( team_id TEXT, bundle_id TEXT, allowed BOOLEAN, payload_uuid TEXT, PRIMARY KEY (team_id, bundle_id) );
144
+ CREATE TABLE settings ( name TEXT, value TEXT, PRIMARY KEY (name) );
145
+ COMMIT;
146
+ EOF
147
+ end
148
+ end
149
+ end
150
+
151
+ def automate_user_account_creation
152
+ Logger.info "Configuring the primary user account..." do
153
+ scratch_installer_configuration_file = "#{@scratch_mountpoint}/private/var/db/.InstallerConfiguration"
154
+ File.write scratch_installer_configuration_file, <<~EOF
155
+ <?xml version="1.0" encoding="UTF-8"?>
156
+ <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
157
+ <plist version="1.0">
158
+ <dict>
159
+ <key>Users</key>
160
+ <array>
161
+ <dict>
162
+ <key>admin</key>
163
+ <true/>
164
+ <key>autologin</key>
165
+ <#{@auto_login ? true : false}/>
166
+ <key>fullName</key>
167
+ <string>#{@full_name}</string>
168
+ <key>shortName</key>
169
+ <string>#{@short_name}</string>
170
+ <key>password</key>
171
+ <string>#{@password}</string>
172
+ <key>skipMiniBuddy</key>
173
+ <#{@skip_mini_buddy ? true : false}/>
174
+ </dict>
175
+ </array>
176
+ </dict>
177
+ </plist>
178
+ EOF
179
+ end
180
+ end
181
+
182
+ def automate_vagrant_ssh_key_installation
183
+ if @short_name == "vagrant"
184
+ Logger.info "Installing the default insecure vagrant ssh key..." do
185
+ scratch_rc_installer_cleanup = "#{@scratch_mountpoint}/private/etc/rc.installer_cleanup"
186
+ scratch_rc_vagrant = "#{@scratch_mountpoint}/private/etc/rc.vagrant"
187
+ File.write scratch_rc_installer_cleanup, <<~EOF
188
+ #!/bin/sh
189
+ rm /etc/rc.installer_cleanup
190
+ /etc/rc.vagrant &
191
+ exit 0
192
+ EOF
193
+ FileUtils.chmod 0755, scratch_rc_installer_cleanup
194
+ File.write scratch_rc_vagrant, <<~EOF
195
+ #!/bin/sh
196
+ rm /etc/rc.vagrant
197
+ while [ ! -e /Users/vagrant ]; do
198
+ sleep 1
199
+ done
200
+ if [ ! -e /Users/vagrant/.ssh ]; then
201
+ mkdir /Users/vagrant/.ssh
202
+ chmod 0700 /Users/vagrant/.ssh
203
+ chown `stat -f %u /Users/vagrant` /Users/vagrant/.ssh
204
+ fi
205
+ echo "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key" >> /Users/vagrant/.ssh/authorized_keys
206
+ chmod 0600 /Users/vagrant/.ssh/authorized_keys
207
+ chown `stat -f %u /Users/vagrant` /Users/vagrant/.ssh/authorized_keys
208
+ EOF
209
+ FileUtils.chmod 0755, scratch_rc_vagrant
210
+ end
211
+ end
212
+ end
213
+
214
+ def enable_passwordless_sudo
215
+ Logger.info "Enabling password-less sudo..." do
216
+ scratch_sudoers_d_user_rule_file = "#{@scratch_mountpoint}/private/etc/sudoers.d/#{@short_name}"
217
+ File.write scratch_sudoers_d_user_rule_file, <<~EOF
218
+ #{@short_name} ALL=(ALL) NOPASSWD: ALL
219
+ EOF
220
+ FileUtils.chmod 0440, scratch_sudoers_d_user_rule_file
221
+ end
222
+ end
223
+
224
+ def enable_sshd
225
+ Logger.info "Enabling sshd..." do
226
+ scratch_launchd_disabled_plist = "#{@scratch_mountpoint}/private/var/db/com.apple.xpc.launchd/disabled.plist"
227
+ opts = @debug ? {} : { :out => File::NULL }
228
+ Task.run %W[ /usr/libexec/PlistBuddy -c #{'Add :com.openssh.sshd bool False'} #{scratch_launchd_disabled_plist} ] + [opts]
229
+ end
230
+ end
231
+
232
+
233
+ def enable_hidpi
234
+ if @hidpi
235
+ Logger.info "Enabling HiDPI resolutions..." do
236
+ scratch_windowserver_preferences = "#{@scratch_mountpoint}/Library/Preferences/com.apple.windowserver.plist"
237
+ File.write scratch_windowserver_preferences, <<~EOF
238
+ <?xml version="1.0" encoding="UTF-8"?>
239
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
240
+ <plist version="1.0">
241
+ <dict>
242
+ <key>DisplayResolutionEnabled</key>
243
+ <true/>
244
+ </dict>
245
+ </plist>
246
+ EOF
247
+ end
248
+ end
249
+ end
250
+
251
+ def save_image
252
+ Logger.info "Saving the image..." do
253
+ Task.run %W[ hdiutil detach -quiet #{@scratch_mountpoint} ]
254
+ FileUtils.mv @scratch_image, "#{@temp_dir}/macinbox.dmg"
255
+ FileUtils.chown ENV["SUDO_USER"], nil, "#{@temp_dir}/macinbox.dmg"
256
+ FileUtils.mv "#{@temp_dir}/macinbox.dmg", @output_path
257
+ end
258
+ end
259
+
260
+ end
261
+
262
+ end
263
+
264
+ end
@@ -0,0 +1,71 @@
1
+ require 'fileutils'
2
+ require 'shellwords'
3
+
4
+ require 'macinbox/error'
5
+ require 'macinbox/logger'
6
+ require 'macinbox/task'
7
+
8
+ module Macinbox
9
+
10
+ module Actions
11
+
12
+ class CreateVMDKFromImage
13
+
14
+ def initialize(opts)
15
+ @input_image = opts[:image_path] or raise ArgumentError.new(":image_path not specified")
16
+ @output_path = opts[:vmdk_path] or raise ArgumentError.new(":vmdk_path not specified")
17
+ @vmware_fusion_app = opts[:vmware_path] or raise ArgumentError.new(":vmware_path not specified")
18
+
19
+ @collector = opts[:collector] or raise ArgumentError.new(":collector not specified")
20
+ @debug = opts[:debug]
21
+
22
+ raise Macinbox::Error.new("input image not found") unless File.exist? @input_image
23
+ raise Macinbox::Error.new("VMware Fusion not found") unless File.exist? @vmware_fusion_app
24
+ end
25
+
26
+ def run
27
+ @temp_dir = Task.backtick %W[ /usr/bin/mktemp -d -t create_vmdk_from_image ]
28
+ @collector.add_temp_dir @temp_dir
29
+
30
+ Logger.info "Mounting the image..." do
31
+
32
+ @collector.on_cleanup do
33
+ %x( hdiutil detach -quiet -force #{@mountpoint.shellescape} > /dev/null 2>&1 ) if @mountpoint
34
+ %x( diskutil eject #{@device.shellescape} > /dev/null 2>&1 ) if @device
35
+ end
36
+
37
+ @mountpoint = "#{@temp_dir}/image_mountpoint"
38
+
39
+ FileUtils.mkdir @mountpoint
40
+
41
+ @device = %x(
42
+ hdiutil attach #{@input_image.shellescape} -mountpoint #{@mountpoint.shellescape} -nobrowse -owners on |
43
+ grep _partition_scheme |
44
+ cut -f1 |
45
+ tr -d [:space:]
46
+ )
47
+
48
+ raise Macinbox::Error.new("failed to mount the image") unless File.exist? @device
49
+ end
50
+
51
+ Logger.info "Converting the image to VMDK format..." do
52
+ rawdiskCreator = "#{@vmware_fusion_app}/Contents/Library/vmware-rawdiskCreator"
53
+ vdiskmanager = "#{@vmware_fusion_app}/Contents/Library/vmware-vdiskmanager"
54
+ Dir.chdir(@temp_dir) do
55
+ Task.run %W[ #{rawdiskCreator} create #{@device} fullDevice rawdisk lsilogic ]
56
+ Task.run %W[ #{vdiskmanager} -t 0 -r rawdisk.vmdk macinbox.vmdk ]
57
+ end
58
+ end
59
+
60
+ Logger.info "Moving the VMDK to the destination..." do
61
+ FileUtils.chown ENV["SUDO_USER"], nil, "#{@temp_dir}/macinbox.vmdk"
62
+ FileUtils.mv "#{@temp_dir}/macinbox.vmdk", @output_path
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+
71
+ end
@@ -0,0 +1,37 @@
1
+ require 'fileutils'
2
+
3
+ require 'macinbox/error'
4
+ require 'macinbox/logger'
5
+
6
+ module Macinbox
7
+
8
+ module Actions
9
+
10
+ class InstallBox
11
+
12
+ def initialize(opts)
13
+ @input_box = opts[:box_path] or raise ArgumentError.new(":box_path not specified")
14
+ @box_name = opts[:box_name] or raise ArgumentError.new(":box_name not specified")
15
+ @debug = opts[:debug]
16
+ raise Macinbox::Error.new("box not found") unless File.exist? @input_box
17
+ end
18
+
19
+ def run
20
+ Logger.info "Copying box to ~/.vagrant.d/boxes..." do
21
+ vagrant_boxes_dir = File.expand_path "~/.vagrant.d/boxes"
22
+ raise Macinbox::Error.new("~/.vagrant.d/boxes not found") unless File.exist? vagrant_boxes_dir
23
+ box_name = @box_name
24
+ box_version = Dir["#{vagrant_boxes_dir}/#{box_name}/*"].map { |o| File.basename(o).to_i }.sort.last.next rescue 0
25
+ box_provider = "vmware_fusion"
26
+ target_box_dir = "#{vagrant_boxes_dir}/#{box_name}/#{box_version}/#{box_provider}"
27
+ raise Macinbox::Error.new("box already exists") if File.exist? target_box_dir
28
+ FileUtils.mkdir_p target_box_dir
29
+ FileUtils.cp Dir["#{@input_box}/*"], target_box_dir
30
+ FileUtils.chown_R ENV["SUDO_USER"], nil, "#{vagrant_boxes_dir}/#{box_name}"
31
+ Logger.info "Installed box: #{box_name} (#{box_provider}, #{box_version})"
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,111 @@
1
+ require 'macinbox/cli/options'
2
+
3
+ require 'pathname'
4
+ require 'fileutils'
5
+
6
+ require "macinbox/actions"
7
+ require "macinbox/collector"
8
+ require 'macinbox/error'
9
+ require 'macinbox/logger'
10
+ require 'macinbox/tty'
11
+
12
+ module Macinbox
13
+
14
+ class CLI
15
+
16
+ def self.run!(argv)
17
+ begin
18
+ self.new.start(argv)
19
+ rescue Macinbox::Error => e
20
+ Logger.error "Error: " + e.to_s
21
+ exit 1
22
+ end
23
+ exit 0
24
+ end
25
+
26
+ def start(argv)
27
+
28
+ parse_options(argv)
29
+
30
+ check_for_sudo_root
31
+
32
+ if not File.exists? @options[:installer_path]
33
+ raise Macinbox::Error.new("Installer app not found: #{@options[:installer_path]}")
34
+ end
35
+
36
+ if not File.exists? @options[:vmware_path]
37
+ raise Macinbox::Error.new("VMware Fusion app not found: #{@options[:vmware_path]}")
38
+ end
39
+
40
+ root_temp_dir = Task.backtick %W[ /usr/bin/mktemp -d -t macinbox_root_temp ]
41
+ user_temp_dir = Task.backtick %W[ sudo -u #{ENV["SUDO_USER"]} /usr/bin/mktemp -d -t macinbox_user_temp ]
42
+
43
+ collector = Collector.new
44
+
45
+ collector.add_temp_dir root_temp_dir
46
+ collector.add_temp_dir user_temp_dir
47
+
48
+ collector.on_cleanup do
49
+ if @options[:debug]
50
+ temp_dir_args = collector.temp_dirs.reverse.map { |o| o.shellescape }.join(" \\\n")
51
+ Logger.error "WARNING: Temporary files were not removed. Run this command to remove them:"
52
+ Logger.error "sudo rm -rf #{temp_dir_args}"
53
+ else
54
+ collector.remove_temp_dirs
55
+ end
56
+ STDERR.print TTY::CURSOR_NORMAL
57
+ end
58
+
59
+ ["TERM", "INT", "EXIT"].each do |signal|
60
+ trap signal do
61
+ trap signal, "SYSTEM_DEFAULT" unless signal == "EXIT"
62
+ Process.waitall
63
+ Logger.reset_depth
64
+ if @success
65
+ Logger.info "Cleaning up..."
66
+ else
67
+ STDERR.puts
68
+ Logger.error "Cleaning up..."
69
+ end
70
+ collector.cleanup!
71
+ Process.kill(signal, Process.pid) unless signal == "EXIT"
72
+ end
73
+ end
74
+
75
+ @options[:image_path] = "macinbox.dmg"
76
+ @options[:vmdk_path] = "macinbox.vmdk"
77
+ @options[:box_path] = "macinbox.box"
78
+ @options[:collector] = collector
79
+
80
+ Dir.chdir(root_temp_dir) do
81
+
82
+ Logger.info "Creating image from installer..." do
83
+ Actions::CreateImageFromInstaller.new(@options).run
84
+ end
85
+
86
+ Logger.info "Creating VMDK from image..." do
87
+ Actions::CreateVMDKFromImage.new(@options).run
88
+ end
89
+
90
+ Logger.info "Creating box from VMDK..." do
91
+ Actions::CreateBoxFromVMDK.new(@options).run
92
+ end
93
+
94
+ Logger.info "Installing box..." do
95
+ Actions::InstallBox.new(@options).run
96
+ end
97
+
98
+ end
99
+
100
+ @success = true
101
+
102
+ end
103
+
104
+ def check_for_sudo_root
105
+ if Process.uid != 0 or ENV["SUDO_USER"].nil?
106
+ raise Macinbox::Error.new("script must be run as root with sudo")
107
+ end
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,59 @@
1
+ require 'optparse'
2
+
3
+ module Macinbox
4
+
5
+ class CLI
6
+
7
+ DEFAULT_OPTION_VALUES = {
8
+ :box_name => "macinbox",
9
+ :disk_size => 64,
10
+ :memory_size => 2048,
11
+ :cpu_count => 2,
12
+ :short_name => "vagrant",
13
+ :installer_path => "/Applications/Install macOS High Sierra.app",
14
+ :vmware_path => "/Applications/VMware Fusion.app",
15
+ :auto_login => true,
16
+ :skip_mini_buddy => true,
17
+ :hidpi => true,
18
+ :fullscreen => true,
19
+ :gui => true,
20
+ :debug => false,
21
+ }
22
+
23
+ def parse_options(argv)
24
+
25
+ @options = DEFAULT_OPTION_VALUES
26
+
27
+ @option_parser = OptionParser.new do |o|
28
+
29
+ o.on('-n', '--name NAME', 'Name of the box (default: macinbox)') { |v| @options[:box_name] = v }
30
+ o.on('-d', '--disk SIZE', 'Size (GB) of the disk (default: 64)') { |v| @options[:disk_size] = v }
31
+ o.on('-m', '--memory SIZE', 'Size (MB) of the memory (default: 2048)') { |v| @options[:memory_size] = v }
32
+ o.on('-c', '--cpu COUNT', 'Number of virtual cores (default: 2)') { |v| @options[:cpu_count] = v }
33
+ o.on('-s', '--short NAME', 'Short name of the user (default: vagrant)') { |v| @options[:short_name] = v }
34
+ o.on('-f', '--full NAME', 'Full name of the user (default: Vagrant)') { |v| @options[:full_name] = v }
35
+ o.on('-p', '--password PASSWORD', 'Password of the user (default: vagrant)') { |v| @options[:password] = v }
36
+
37
+ o.on( '--installer PATH', 'Path to the macOS installer app') { |v| @options[:installer_path] = v }
38
+ o.on( '--vmware PATH', 'Path to the VMware Fusion app') { |v| @options[:vmware_path] = v }
39
+
40
+ o.on( '--no-auto-login', 'Disable auto login') { |v| @options[:auto_login] = v }
41
+ o.on( '--no-skip-mini-buddy', 'Show the mini buddy on first login') { |v| @options[:skip_mini_buddy] = v }
42
+ o.on( '--no-hidpi', 'Disable HiDPI resolutions') { |v| @options[:hidpi] = v }
43
+ o.on( '--no-fullscreen', 'Display the virtual machine GUI in a window') { |v| @options[:fullsceen] = v }
44
+ o.on( '--no-gui', 'Disable the GUI') { |v| @options[:gui] = v }
45
+ o.on( '--debug', 'Enable debug mode') { |v| @options[:debug] = v }
46
+
47
+ o.on('-v', '--version') { puts "macinbox #{Macinbox::VERSION}"; exit }
48
+ o.on('-h', '--help') { puts o; exit }
49
+
50
+ o.parse(argv)
51
+
52
+ end
53
+
54
+ @options[:full_name] ||= @options[:short_name].capitalize
55
+ @options[:password] ||= @options[:short_name]
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,31 @@
1
+ require 'fileutils'
2
+
3
+ module Macinbox
4
+ class Collector
5
+ def initialize
6
+ @temp_dirs = []
7
+ @blocks = []
8
+ end
9
+ def add_temp_dir(temp_dir)
10
+ @temp_dirs << temp_dir
11
+ end
12
+ def temp_dirs
13
+ @temp_dirs
14
+ end
15
+ def remove_temp_dirs
16
+ @temp_dirs.reverse.each do |temp_dir|
17
+ FileUtils.remove_dir(temp_dir)
18
+ end
19
+ end
20
+ def on_cleanup(&block)
21
+ @blocks << block
22
+ end
23
+ def cleanup!
24
+ @blocks.reverse.each do |block|
25
+ block.call
26
+ end
27
+ @blocks = []
28
+ @temp_dirs = []
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,4 @@
1
+ module Macinbox
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,26 @@
1
+ require 'macinbox/tty'
2
+
3
+ module Macinbox
4
+ class Logger
5
+ include TTY
6
+ PREFIXES = ["• ", " + ", " - "]
7
+ @@depth = 0
8
+ def self.prefix
9
+ PREFIXES[@@depth]
10
+ end
11
+ def self.reset_depth
12
+ @@depth = 0
13
+ end
14
+ def self.info(msg)
15
+ STDERR.puts GREEN + prefix + msg + BLACK
16
+ if block_given?
17
+ @@depth += 1
18
+ yield
19
+ @@depth -= 1
20
+ end
21
+ end
22
+ def self.error(msg)
23
+ STDERR.puts RED + prefix + msg + BLACK
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,82 @@
1
+ require 'macinbox/error'
2
+ require 'macinbox/logger'
3
+ require 'macinbox/tty'
4
+
5
+ module Macinbox
6
+
7
+ class Task
8
+
9
+ include TTY
10
+
11
+ def self.run(cmd)
12
+ system(*cmd) or raise Macinbox::Error.new("#{cmd.slice(0)} failed with non-zero exit code: #{$?.to_i}")
13
+ end
14
+
15
+ def self.run_as_sudo_user(cmd)
16
+ system "sudo", "-u", ENV["SUDO_USER"], *cmd or raise Macinbox::Error.new("#{cmd.slice(0)} failed with non-zero exit code: #{$?.to_i}")
17
+ end
18
+
19
+ def self.progress_bar(activity, percent_done)
20
+ @spinner ||= Enumerator.new { |e| loop { e.yield '|'; e.yield '/'; e.yield '-'; e.yield '\\' } }
21
+ columns = STDOUT.winsize[1] - 8
22
+ header = activity + ": " + percent_done.round(0).to_s + "% done "
23
+ bar = ""
24
+ if percent_done.round(0).to_i < 100
25
+ bar_available_size = columns - header.size - 2
26
+ bar_size = (percent_done * bar_available_size / 100.0).to_i
27
+ bar_remainder = bar_available_size - bar_size
28
+ bar_full = "#" * bar_size
29
+ bar_empty = @spinner.next + " " * (bar_remainder-1) rescue ""
30
+ bar = "[" + bar_full + bar_empty + "]"
31
+ end
32
+ header + bar
33
+ end
34
+
35
+ def self.run_with_progress(activity, cmd, opts={})
36
+ STDERR.print CURSOR_INVISIBLE
37
+ STDERR.print CLEAR_LINE + GREEN + progress_bar(activity, 0.0) + BLACK
38
+ IO.popen cmd, opts do |pipe|
39
+ pipe.each_line do |line|
40
+ percent = yield line
41
+ STDERR.print CLEAR_LINE + GREEN + progress_bar(activity, percent) + BLACK if percent
42
+ end
43
+ end
44
+ STDERR.puts CURSOR_NORMAL
45
+ end
46
+
47
+ def self.write_file_to_io_with_progress(source, destination)
48
+ activity = Logger.prefix + File.basename(source)
49
+ eof = false
50
+ bytes_written = 0
51
+ total_size = File.size(source)
52
+ last_percent_done = -1
53
+ STDERR.print CURSOR_INVISIBLE
54
+ STDERR.print CLEAR_LINE + GREEN + progress_bar(activity, 0.0) + BLACK
55
+ File.open(source) do |file|
56
+ until eof
57
+ begin
58
+ bytes_written += destination.write(file.readpartial(1024*1024))
59
+ percent_done = ((bytes_written.to_f / total_size.to_f) * 100).round(1)
60
+ last_percent_done = percent_done
61
+ STDERR.print CLEAR_LINE + GREEN + progress_bar(activity, percent_done) + BLACK
62
+ rescue EOFError
63
+ eof = true
64
+ end
65
+ end
66
+ end
67
+ STDERR.puts CURSOR_NORMAL
68
+ end
69
+
70
+ def self.backtick(cmd)
71
+ IO.popen(cmd).read.chomp
72
+ end
73
+
74
+ def self.run_with_input(cmd)
75
+ IO.popen(cmd, "w") do |pipe|
76
+ yield pipe
77
+ end
78
+ $? == 0 or raise Macinbox::Error.new("#{cmd.slice(0)} failed with non-zero exit code: #{$?.to_i}")
79
+ end
80
+ end
81
+
82
+ end
@@ -0,0 +1,10 @@
1
+ module Macinbox
2
+ module TTY
3
+ BLACK = %x(tput setaf 0)
4
+ RED = %x(tput setaf 1)
5
+ GREEN = %x(tput setaf 2)
6
+ CLEAR_LINE = "\r" + %x( tput el )
7
+ CURSOR_INVISIBLE = %x( tput civis )
8
+ CURSOR_NORMAL = %x( tput cnorm )
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module Macinbox
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,32 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "macinbox/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "macinbox"
7
+ s.version = Macinbox::VERSION
8
+ s.authors = ["David Kramer"]
9
+ s.email = ["bacongravy@icloud.com"]
10
+
11
+ s.summary = "Puts macOS in a Vagrant VMware Fusion box"
12
+ s.homepage = "https://github.com/bacongravy/macinbox"
13
+ s.license = "MIT"
14
+
15
+ s.files = `git ls-files -z`.split("\x0").reject do |f|
16
+ f.match(%r{^(test|spec|features)/})
17
+ end
18
+ s.bindir = "exe"
19
+ s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency "bundler", "~> 1.16"
23
+ s.add_development_dependency "rake", "~> 10.0"
24
+
25
+ s.required_ruby_version = '~> 2.3'
26
+
27
+ s.requirements << "macOS High Sierra"
28
+ s.requirements << "macOS High Sierra installer app"
29
+ s.requirements << "Vagrant"
30
+ s.requirements << "VMware Fusion"
31
+
32
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: macinbox
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - David Kramer
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-02-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description:
42
+ email:
43
+ - bacongravy@icloud.com
44
+ executables:
45
+ - macinbox
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".gitignore"
50
+ - Gemfile
51
+ - Gemfile.lock
52
+ - LICENSE
53
+ - README.md
54
+ - Rakefile
55
+ - Vagrantfile
56
+ - bin/console
57
+ - bin/setup
58
+ - exe/macinbox
59
+ - lib/macinbox.rb
60
+ - lib/macinbox/actions.rb
61
+ - lib/macinbox/actions/create_box_from_vmdk.rb
62
+ - lib/macinbox/actions/create_image_from_installer.rb
63
+ - lib/macinbox/actions/create_vmdk_from_image.rb
64
+ - lib/macinbox/actions/install_box.rb
65
+ - lib/macinbox/cli.rb
66
+ - lib/macinbox/cli/options.rb
67
+ - lib/macinbox/collector.rb
68
+ - lib/macinbox/error.rb
69
+ - lib/macinbox/logger.rb
70
+ - lib/macinbox/task.rb
71
+ - lib/macinbox/tty.rb
72
+ - lib/macinbox/version.rb
73
+ - macinbox.gemspec
74
+ homepage: https://github.com/bacongravy/macinbox
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - "~>"
85
+ - !ruby/object:Gem::Version
86
+ version: '2.3'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements:
93
+ - macOS High Sierra
94
+ - macOS High Sierra installer app
95
+ - Vagrant
96
+ - VMware Fusion
97
+ rubyforge_project:
98
+ rubygems_version: 2.5.2
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: Puts macOS in a Vagrant VMware Fusion box
102
+ test_files: []