macinbox 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +20 -0
- data/LICENSE +23 -0
- data/README.md +156 -0
- data/Rakefile +2 -0
- data/Vagrantfile +3 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/macinbox +5 -0
- data/lib/macinbox.rb +8 -0
- data/lib/macinbox/actions.rb +4 -0
- data/lib/macinbox/actions/create_box_from_vmdk.rb +144 -0
- data/lib/macinbox/actions/create_image_from_installer.rb +264 -0
- data/lib/macinbox/actions/create_vmdk_from_image.rb +71 -0
- data/lib/macinbox/actions/install_box.rb +37 -0
- data/lib/macinbox/cli.rb +111 -0
- data/lib/macinbox/cli/options.rb +59 -0
- data/lib/macinbox/collector.rb +31 -0
- data/lib/macinbox/error.rb +4 -0
- data/lib/macinbox/logger.rb +26 -0
- data/lib/macinbox/task.rb +82 -0
- data/lib/macinbox/tty.rb +10 -0
- data/lib/macinbox/version.rb +3 -0
- data/macinbox.gemspec +32 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
data/Vagrantfile
ADDED
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
data/exe/macinbox
ADDED
data/lib/macinbox.rb
ADDED
@@ -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
|
data/lib/macinbox/cli.rb
ADDED
@@ -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,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
|
data/lib/macinbox/tty.rb
ADDED
data/macinbox.gemspec
ADDED
@@ -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: []
|