├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── vagrant-unison.rb └── vagrant-unison │ ├── command.rb │ ├── config.rb │ ├── errors.rb │ ├── plugin.rb │ ├── shell_command.rb │ ├── ssh_command.rb │ ├── unison_paths.rb │ ├── unison_sync.rb │ └── version.rb ├── locales └── en.yml ├── spec └── vagrant-unison │ └── config_spec.rb └── vagrant-unison.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | # OS-specific 2 | .DS_Store 3 | 4 | # Bundler/Rubygems 5 | *.gem 6 | .bundle 7 | pkg/* 8 | tags 9 | Gemfile.lock 10 | 11 | # Vagrant 12 | .vagrant 13 | Vagrantfile 14 | !example_box/Vagrantfile 15 | 16 | # IDEs 17 | .project 18 | .idea 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 (March, 2013) 2 | 3 | * Initial release. 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | # We depend on Vagrant for development, but we don't add it as a 7 | # gem dependency because we expect to be installed within the 8 | # Vagrant environment itself using `vagrant plugin`. 9 | gem "vagrant", :git => "git://github.com/mitchellh/vagrant.git" 10 | end 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2013 Mitchell Hashimoto 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vagrant Unison Plugin 2 | 3 | This is a [Vagrant](http://www.vagrantup.com) 1.1+ plugin that syncs files over SSH from a local folder 4 | to your Vagrant VM (local or on AWS). Under the covers it uses [Unison](http://www.cis.upenn.edu/~bcpierce/unison/) 5 | 6 | **NOTE:** This plugin requires Vagrant 1.1+, 7 | 8 | ## Features 9 | 10 | * Unisoned folder support via `unison` over `ssh` -> will work with any vagrant provider, eg Virtualbox or AWS. 11 | 12 | ## Usage 13 | 14 | 1. You must already have [Unison](http://www.cis.upenn.edu/~bcpierce/unison/) installed and in your path. 15 | * On Mac you can install this with Homebrew: `brew install unison` (on Yosemite you will have to use https://rudix-mountainlion.googlecode.com/files/unison-2.40.102-0.pkg) 16 | * On Unix (Ubuntu) install using `sudo apt-get install unison` 17 | * On Windows, download [2.40.102](http://alan.petitepomme.net/unison/assets/Unison-2.40.102.zip), unzip, rename `Unison-2.40.102 Text.exe` to `unison.exe` and copy to somewhere in your path. 18 | 1. Install using standard Vagrant 1.1+ plugin installation methods. 19 | ``` 20 | $ vagrant plugin install vagrant-unison 21 | ``` 22 | 1. After installing, edit your Vagrantfile and add a configuration directive similar to the below: 23 | ``` 24 | Vagrant.configure("2") do |config| 25 | config.vm.box = "dummy" 26 | 27 | config.sync.host_folder = "src/" #relative to the folder your Vagrantfile is in 28 | config.sync.guest_folder = "src/" #relative to the vagrant home folder -> /home/vagrant 29 | config.sync.ignore = "Name {.idea,.DS_Store}" 30 | 31 | end 32 | ``` 33 | 1. Start up your starting your vagrant box as normal (eg: `vagrant up`) 34 | 35 | ## Start syncing Folders 36 | 37 | Run `vagrant sync` to start watching the local_folder for changes, and syncing these to your vagrang VM. 38 | 39 | Under the covers this uses your system installation of [Unison](http://www.cis.upenn.edu/~bcpierce/unison/), 40 | which must be installed in your path. 41 | 42 | ## Start syncing Folders in repeat mode 43 | 44 | Run `vagrant sync-repeat` to start in bidirect monitor (repeat) mode. 45 | 46 | ## Start syncing Folders in interactive mode 47 | 48 | Run `vagrant sync-interact` to start in interactive mode that allows solving conflicts. 49 | 50 | ## Cleanup unison database 51 | When you get 52 | ``` 53 | Fatal error: Warning: inconsistent state. 54 | The archive file is missing on some hosts. 55 | For safety, the remaining copies should be deleted. 56 | Archive arb126d8de1ef26a835b94cf51975c530f on host blablabla.local should be DELETED 57 | Archive arbc6a36f85b3d1473c55565dd220acf68 on host blablabla is MISSING 58 | Please delete archive files as appropriate and try again 59 | or invoke Unison with -ignorearchives flag. 60 | ``` 61 | 62 | Run `vagrant sync-cleanup` to clear Archive from ~/Library/Application Support/Unison/ and files from host folder. 63 | Running Unison with -ignorearchives flag is a bad idea, since it will produce conflicts. 64 | 65 | ## Development 66 | 67 | To work on the `vagrant-unison` plugin, clone this repository out, and use 68 | [Bundler](http://gembundler.com) to get the dependencies: 69 | 70 | ``` 71 | $ bundle 72 | ``` 73 | 74 | Once you have the dependencies, verify the unit tests pass with `rake`: 75 | 76 | ``` 77 | $ bundle exec rake 78 | ``` 79 | 80 | If those pass, you're ready to start developing the plugin. You can test 81 | the plugin without installing it into your Vagrant environment by just 82 | creating a `Vagrantfile` in the top level of this directory (it is gitignored) 83 | that uses it, and uses bundler to execute Vagrant: 84 | 85 | ``` 86 | $ bundle exec vagrant up 87 | $ bundle exec vagrant sync 88 | ``` 89 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rspec/core/rake_task' 4 | 5 | # Immediately sync all stdout so that tools like buildbot can 6 | # immediately load in the output. 7 | $stdout.sync = true 8 | $stderr.sync = true 9 | 10 | # Change to the directory of this file. 11 | Dir.chdir(File.expand_path("../", __FILE__)) 12 | 13 | # This installs the tasks that help with gem creation and 14 | # publishing. 15 | Bundler::GemHelper.install_tasks 16 | 17 | # Install the `spec` task so that we can run tests. 18 | RSpec::Core::RakeTask.new 19 | 20 | # Default task is to run the unit tests 21 | task :default => "spec" 22 | -------------------------------------------------------------------------------- /lib/vagrant-unison.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | 3 | require "vagrant-unison/plugin" 4 | require "vagrant-unison/errors" 5 | 6 | module VagrantPlugins 7 | module Unison 8 | # This returns the path to the source of this plugin. 9 | # 10 | # @return [Pathname] 11 | def self.source_root 12 | @source_root ||= Pathname.new(File.expand_path("../../", __FILE__)) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/vagrant-unison/command.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | require "vagrant" 3 | require "thread" 4 | require 'listen' 5 | 6 | require_relative 'unison_paths' 7 | require_relative 'ssh_command' 8 | require_relative 'shell_command' 9 | require_relative 'unison_sync' 10 | 11 | module VagrantPlugins 12 | module Unison 13 | class Command < Vagrant.plugin("2", :command) 14 | include UnisonSync 15 | 16 | def execute 17 | with_target_vms do |machine| 18 | paths = UnisonPaths.new(@env, machine) 19 | host_path = paths.host 20 | 21 | sync(machine, paths) 22 | 23 | @env.ui.info "Watching #{host_path} for changes..." 24 | 25 | listener = Listen.to(host_path) do |modified, added, removed| 26 | @env.ui.info "Detected modifications to #{modified.inspect}" unless modified.empty? 27 | @env.ui.info "Detected new files #{added.inspect}" unless added.empty? 28 | @env.ui.info "Detected deleted files #{removed.inspect}" unless removed.empty? 29 | 30 | sync(machine, paths) 31 | end 32 | 33 | queue = Queue.new 34 | 35 | callback = lambda do 36 | # This needs to execute in another thread because Thread 37 | # synchronization can't happen in a trap context. 38 | Thread.new { queue << true } 39 | end 40 | 41 | # Run the listener in a busy block so that we can cleanly 42 | # exit once we receive an interrupt. 43 | Vagrant::Util::Busy.busy(callback) do 44 | listener.start 45 | queue.pop 46 | listener.stop if listener.listen? 47 | end 48 | end 49 | 50 | 0 51 | end 52 | 53 | def sync(machine, paths) 54 | execute_sync_command(machine) do |command| 55 | command.batch = true 56 | 57 | @env.ui.info "Running #{command.to_s}" 58 | 59 | r = Vagrant::Util::Subprocess.execute(*command.to_a) 60 | 61 | case r.exit_code 62 | when 0 63 | @env.ui.info "Unison completed succesfully" 64 | when 1 65 | @env.ui.info "Unison completed - all file transfers were successful; some files were skipped" 66 | when 2 67 | @env.ui.info "Unison completed - non-fatal failures during file transfer: #{r.stderr}" 68 | else 69 | raise Vagrant::Errors::UnisonError, 70 | :command => command.to_s, 71 | :guestpath => paths.guest, 72 | :hostpath => paths.host, 73 | :stderr => r.stderr 74 | end 75 | end 76 | end 77 | end 78 | 79 | class CommandRepeat < Vagrant.plugin("2", :command) 80 | include UnisonSync 81 | 82 | def execute 83 | with_target_vms do |machine| 84 | execute_sync_command(machine) do |command| 85 | command.repeat = true 86 | command.terse = true 87 | command = command.to_s 88 | 89 | @env.ui.info "Running #{command}" 90 | 91 | system(command) 92 | end 93 | end 94 | 95 | 0 96 | end 97 | end 98 | 99 | class CommandCleanup < Vagrant.plugin("2", :command) 100 | include UnisonSync 101 | 102 | def execute 103 | with_target_vms do |machine| 104 | guest_path = UnisonPaths.new(@env, machine).guest 105 | 106 | command = "rm -rf ~/Library/'Application Support'/Unison/*" 107 | @env.ui.info "Running #{command} on host" 108 | system(command) 109 | 110 | command = "rm -rf #{guest_path}" 111 | @env.ui.info "Running #{command} on guest VM" 112 | machine.communicate.sudo(command) 113 | 114 | command = "rm -rf ~/.unison" 115 | @env.ui.info "Running #{command} on guest VM" 116 | machine.communicate.sudo(command) 117 | end 118 | 119 | 0 120 | end 121 | end 122 | 123 | class CommandInteract < Vagrant.plugin("2", :command) 124 | include UnisonSync 125 | 126 | def execute 127 | with_target_vms do |machine| 128 | execute_sync_command(machine) do |command| 129 | command.terse = true 130 | command = command.to_s 131 | 132 | @env.ui.info "Running #{command}" 133 | 134 | system(command) 135 | end 136 | end 137 | 138 | 0 139 | end 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/vagrant-unison/config.rb: -------------------------------------------------------------------------------- 1 | require "vagrant" 2 | 3 | module VagrantPlugins 4 | module Unison 5 | class Config < Vagrant.plugin("2", :config) 6 | # Host Folder to Sync 7 | # 8 | # @return [String] 9 | attr_accessor :host_folder 10 | 11 | # Guest Folder to Sync. 12 | # 13 | # @return [String] 14 | attr_accessor :guest_folder 15 | 16 | # Pattern of files to ignore. 17 | # 18 | # @return [String] 19 | attr_accessor :ignore 20 | 21 | # Repeat speed. 22 | # 23 | # @return [String] 24 | attr_accessor :repeat 25 | 26 | def initialize(region_specific=false) 27 | @host_folder = UNSET_VALUE 28 | @remote_folder = UNSET_VALUE 29 | @ignore = UNSET_VALUE 30 | @repeat = UNSET_VALUE 31 | end 32 | 33 | #------------------------------------------------------------------- 34 | # Internal methods. 35 | #------------------------------------------------------------------- 36 | 37 | # def merge(other) 38 | # super.tap do |result| 39 | # # TODO - do something sensible; current last config wins 40 | # result.local_folder = other.local_folder 41 | # result.remote_folder = other.remote_folder 42 | # end 43 | # end 44 | 45 | def finalize! 46 | # The access keys default to nil 47 | @host_folder = nil if @host_folder == UNSET_VALUE 48 | @guest_folder = nil if @guest_folder == UNSET_VALUE 49 | @ignore = nil if @ignore == UNSET_VALUE 50 | @repeat = 1 if @repeat == UNSET_VALUE 51 | 52 | # Mark that we finalized 53 | @__finalized = true 54 | end 55 | 56 | def validate(machine) 57 | errors = [] 58 | 59 | errors << I18n.t("vagrant_sync.config.host_folder_required") if @host_folder.nil? 60 | errors << I18n.t("vagrant_sync.config.guest_folder_required") if @guest_folder.nil? 61 | 62 | { "Unison" => errors } 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/vagrant-unison/errors.rb: -------------------------------------------------------------------------------- 1 | require "vagrant" 2 | 3 | module Vagrant 4 | module Errors 5 | class UnisonError < VagrantError 6 | error_key(:unison_error, "vagrant_unison.errors") 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/vagrant-unison/plugin.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require "vagrant" 3 | rescue LoadError 4 | raise "The vagrant-unison plugin must be run within Vagrant." 5 | end 6 | 7 | # This is a sanity check to make sure no one is attempting to install 8 | # this into an early Vagrant version. 9 | if Vagrant::VERSION < "1.1.0" 10 | raise "The vagrant-unison plugin is only compatible with Vagrant 1.1+" 11 | end 12 | 13 | module VagrantPlugins 14 | module Unison 15 | class Plugin < Vagrant.plugin("2") 16 | name "Unison" 17 | description <<-DESC 18 | This plugin syncs files over SSH from a local folder 19 | to your Vagrant VM (local or on AWS). 20 | DESC 21 | 22 | config "sync" do 23 | require_relative "config" 24 | Config 25 | end 26 | 27 | command "sync" do 28 | # Setup logging and i18n 29 | setup_logging 30 | setup_i18n 31 | 32 | #Return the command 33 | require_relative "command" 34 | Command 35 | end 36 | 37 | command "sync-repeat" do 38 | # Setup logging and i18n 39 | setup_logging 40 | setup_i18n 41 | 42 | #Return the command 43 | require_relative "command" 44 | CommandRepeat 45 | end 46 | 47 | command "sync-cleanup" do 48 | # Setup logging and i18n 49 | setup_logging 50 | setup_i18n 51 | 52 | #Return the command 53 | require_relative "command" 54 | CommandCleanup 55 | end 56 | 57 | command "sync-interact" do 58 | # Setup logging and i18n 59 | setup_logging 60 | setup_i18n 61 | 62 | #Return the command 63 | require_relative "command" 64 | CommandInteract 65 | end 66 | 67 | # This initializes the internationalization strings. 68 | def self.setup_i18n 69 | I18n.load_path << File.expand_path("locales/en.yml", Unison.source_root) 70 | I18n.reload! 71 | end 72 | 73 | # This sets up our log level to be whatever VAGRANT_LOG is. 74 | def self.setup_logging 75 | require "log4r" 76 | 77 | level = nil 78 | begin 79 | level = Log4r.const_get(ENV["VAGRANT_LOG"].upcase) 80 | rescue NameError 81 | # This means that the logging constant wasn't found, 82 | # which is fine. We just keep `level` as `nil`. But 83 | # we tell the user. 84 | level = nil 85 | end 86 | 87 | # Some constants, such as "true" resolve to booleans, so the 88 | # above error checking doesn't catch it. This will check to make 89 | # sure that the log level is an integer, as Log4r requires. 90 | level = nil if !level.is_a?(Integer) 91 | 92 | # Set the logging level on all "vagrant" namespaced 93 | # logs as long as we have a valid level. 94 | if level 95 | logger = Log4r::Logger.new("vagrant_sync") 96 | logger.outputters = Log4r::Outputter.stderr 97 | logger.level = level 98 | logger = nil 99 | end 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/vagrant-unison/shell_command.rb: -------------------------------------------------------------------------------- 1 | module VagrantPlugins 2 | module Unison 3 | class ShellCommand 4 | def initialize machine, paths, ssh_command 5 | @machine = machine 6 | @paths = paths 7 | @ssh_command = ssh_command 8 | end 9 | 10 | attr_accessor :batch, :repeat, :terse 11 | 12 | def to_a 13 | args.map do |arg| 14 | arg = arg[1...-1] if arg =~ /\A"(.*)"\z/ 15 | arg 16 | end 17 | end 18 | 19 | def to_s 20 | args.join(' ') 21 | end 22 | 23 | private 24 | 25 | def args 26 | [ 27 | 'unison', 28 | @paths.host, 29 | @ssh_command.uri, 30 | batch_arg, 31 | terse_arg, 32 | repeat_arg, 33 | ignore_arg, 34 | ['-sshargs', %("#{@ssh_command.command}")], 35 | ].flatten.compact 36 | end 37 | 38 | def batch_arg 39 | '-batch' if batch 40 | end 41 | 42 | def ignore_arg 43 | ['-ignore', %("#{@machine.config.sync.ignore}")] if @machine.config.sync.ignore 44 | end 45 | 46 | def repeat_arg 47 | ['-repeat', @machine.config.sync.repeat] if repeat && @machine.config.sync.repeat 48 | end 49 | 50 | def terse_arg 51 | '-terse' if terse 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/vagrant-unison/ssh_command.rb: -------------------------------------------------------------------------------- 1 | module VagrantPlugins 2 | module Unison 3 | class SshCommand 4 | def initialize(machine, unison_paths) 5 | @machine = machine 6 | @unison_paths = unison_paths 7 | end 8 | 9 | def command 10 | %W( 11 | -p #{ssh_info[:port]} 12 | #{proxy_command} 13 | -o StrictHostKeyChecking=no 14 | -o UserKnownHostsFile=/dev/null 15 | #{key_paths} 16 | ).compact.join(' ') 17 | end 18 | 19 | def uri 20 | username = ssh_info[:username] 21 | host = ssh_info[:host] 22 | 23 | "ssh://#{username}@#{host}/#{@unison_paths.guest}" 24 | end 25 | 26 | private 27 | 28 | def proxy_command 29 | command = ssh_info[:proxy_command] 30 | return nil unless command 31 | "-o ProxyCommand='#{command}'" 32 | end 33 | 34 | def ssh_info 35 | @machine.ssh_info 36 | end 37 | 38 | def key_paths 39 | ssh_info[:private_key_path].map { |p| "-i #{p}" }.join(' ') 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/vagrant-unison/unison_paths.rb: -------------------------------------------------------------------------------- 1 | module VagrantPlugins 2 | module Unison 3 | class UnisonPaths 4 | def initialize(env, machine) 5 | @env = env 6 | @machine = machine 7 | end 8 | 9 | def guest 10 | @machine.config.sync.guest_folder 11 | end 12 | 13 | def host 14 | @host ||= begin 15 | path = File.expand_path(@machine.config.sync.host_folder, @env.root_path) 16 | 17 | # Make sure there is a trailing slash on the host path to 18 | # avoid creating an additional directory with rsync 19 | path = "#{path}/" if path !~ /\/$/ 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/vagrant-unison/unison_sync.rb: -------------------------------------------------------------------------------- 1 | module VagrantPlugins 2 | module Unison 3 | module UnisonSync 4 | def execute_sync_command(machine) 5 | unison_paths = UnisonPaths.new(@env, machine) 6 | guest_path = unison_paths.guest 7 | host_path = unison_paths.host 8 | 9 | @env.ui.info "Unisoning changes from {host}::#{host_path} --> {guest VM}::#{guest_path}" 10 | 11 | # Create the guest path 12 | machine.communicate.sudo("mkdir -p '#{guest_path}'") 13 | machine.communicate.sudo("chown #{machine.ssh_info[:username]} '#{guest_path}'") 14 | 15 | ssh_command = SshCommand.new(machine, unison_paths) 16 | shell_command = ShellCommand.new(machine, unison_paths, ssh_command) 17 | 18 | yield shell_command 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/vagrant-unison/version.rb: -------------------------------------------------------------------------------- 1 | module VagrantPlugins 2 | module Unison 3 | VERSION = "0.0.16" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | vagrant_unison: 3 | 4 | config: 5 | host_folder_required: |- 6 | Host folder is required 7 | guest_folder_required: |- 8 | Guest folder is required 9 | 10 | errors: 11 | unison_error: |- 12 | There was an error when attemping to sync folders using unison. 13 | Please inspect the error message below for more info. 14 | 15 | Host path: %{hostpath} 16 | Guest path: %{guestpath} 17 | Error: %{stderr} 18 | Full command causing error: 19 | %{command} -------------------------------------------------------------------------------- /spec/vagrant-unison/config_spec.rb: -------------------------------------------------------------------------------- 1 | require "vagrant-unison/config" 2 | 3 | describe VagrantPlugins::Unison::Config do 4 | let(:instance) { described_class.new } 5 | 6 | describe "defaults" do 7 | subject do 8 | instance.tap do |o| 9 | o.finalize! 10 | end 11 | end 12 | 13 | its("host_folder") { should be_nil } 14 | its("guest_folder") { should be_nil } 15 | its("ignore") { should be_nil } 16 | end 17 | 18 | describe "overriding defaults" do 19 | # I typically don't meta-program in tests, but this is a very 20 | # simple boilerplate test, so I cut corners here. It just sets 21 | # each of these attributes to "foo" in isolation, and reads the value 22 | # and asserts the proper result comes back out. 23 | [:host_folder, :guest_folder].each do |attribute| 24 | 25 | it "should not default #{attribute} if overridden" do 26 | instance.send("#{attribute}=".to_sym, "foo") 27 | instance.finalize! 28 | instance.send(attribute).should == "foo" 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /vagrant-unison.gemspec: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path("../lib", __FILE__) 2 | require "vagrant-unison/version" 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "vagrant-unison" 6 | s.version = VagrantPlugins::Unison::VERSION 7 | s.platform = Gem::Platform::RUBY 8 | s.authors = "David Laing" 9 | s.email = "david@davidlaing.com" 10 | s.homepage = "http://github.com/mrdavidlaing/vagrant-unison" 11 | s.summary = "Vagrant 1.1 plugin to sync local files to VM over SSH" 12 | s.description = "Vagrant 1.1 plugin to sync local files to VM over SSH using Unison" 13 | 14 | s.required_rubygems_version = ">= 1.3.6" 15 | 16 | s.add_runtime_dependency "listen", ">= 0.7.3" 17 | s.add_runtime_dependency "rb-fsevent", "~> 0.9" 18 | 19 | s.add_development_dependency "rake" 20 | s.add_development_dependency "rspec-core", "~> 2.12.2" 21 | s.add_development_dependency "rspec-expectations", "~> 2.12.1" 22 | s.add_development_dependency "rspec-mocks", "~> 2.12.1" 23 | 24 | # The following block of code determines the files that should be included 25 | # in the gem. It does this by reading all the files in the directory where 26 | # this gemspec is, and parsing out the ignored files from the gitignore. 27 | # Note that the entire gitignore(5) syntax is not supported, specifically 28 | # the "!" syntax, but it should mostly work correctly. 29 | root_path = File.dirname(__FILE__) 30 | all_files = Dir.chdir(root_path) { Dir.glob("**/{*,.*}") } 31 | all_files.reject! { |file| [".", ".."].include?(File.basename(file)) } 32 | gitignore_path = File.join(root_path, ".gitignore") 33 | gitignore = File.readlines(gitignore_path) 34 | gitignore.map! { |line| line.chomp.strip } 35 | gitignore.reject! { |line| line.empty? || line =~ /^(#|!)/ } 36 | 37 | unignored_files = all_files.reject do |file| 38 | # Ignore any directories, the gemspec only cares about files 39 | next true if File.directory?(file) 40 | 41 | # Ignore any paths that match anything in the gitignore. We do 42 | # two tests here: 43 | # 44 | # - First, test to see if the entire path matches the gitignore. 45 | # - Second, match if the basename does, this makes it so that things 46 | # like '.DS_Store' will match sub-directories too (same behavior 47 | # as git). 48 | # 49 | gitignore.any? do |ignore| 50 | File.fnmatch(ignore, file, File::FNM_PATHNAME) || 51 | File.fnmatch(ignore, File.basename(file), File::FNM_PATHNAME) 52 | end 53 | end 54 | 55 | s.files = unignored_files 56 | s.executables = unignored_files.map { |f| f[/^bin\/(.*)/, 1] }.compact 57 | s.require_path = 'lib' 58 | end 59 | --------------------------------------------------------------------------------