├── .rspec ├── VERSION ├── lib ├── librarian │ ├── mock.rb │ ├── mock │ │ ├── source.rb │ │ ├── version.rb │ │ ├── extension.rb │ │ ├── dsl.rb │ │ ├── cli.rb │ │ ├── environment.rb │ │ └── source │ │ │ ├── mock.rb │ │ │ └── mock │ │ │ └── registry.rb │ ├── error.rb │ ├── source.rb │ ├── config.rb │ ├── version.rb │ ├── action.rb │ ├── spec.rb │ ├── specfile.rb │ ├── action │ │ ├── ensure.rb │ │ ├── base.rb │ │ ├── clean.rb │ │ ├── resolve.rb │ │ ├── update.rb │ │ ├── persist_resolution_mixin.rb │ │ └── install.rb │ ├── support │ │ └── abstract_method.rb │ ├── lockfile.rb │ ├── config │ │ ├── hash_source.rb │ │ ├── file_source.rb │ │ ├── source.rb │ │ └── database.rb │ ├── helpers.rb │ ├── source │ │ ├── path.rb │ │ ├── basic_api.rb │ │ ├── local.rb │ │ ├── git.rb │ │ └── git │ │ │ └── repository.rb │ ├── logger.rb │ ├── dsl │ │ ├── receiver.rb │ │ └── target.rb │ ├── linter │ │ └── source_linter.rb │ ├── ui.rb │ ├── resolution.rb │ ├── lockfile │ │ ├── compiler.rb │ │ └── parser.rb │ ├── cli │ │ └── manifest_presenter.rb │ ├── environment │ │ └── runtime_cache.rb │ ├── resolver.rb │ ├── dsl.rb │ ├── rspec │ │ └── support │ │ │ └── cli_macro.rb │ ├── manifest.rb │ ├── manifest_set.rb │ ├── algorithms.rb │ ├── dependency.rb │ ├── posix.rb │ ├── environment.rb │ ├── spec_change_set.rb │ ├── cli.rb │ └── resolver │ │ └── implementation.rb └── librarian.rb ├── .gitignore ├── Gemfile ├── .travis.yml ├── spec ├── unit │ ├── manifest │ │ └── version_spec.rb │ ├── dependency │ │ └── requirement_spec.rb │ ├── action │ │ ├── base_spec.rb │ │ ├── ensure_spec.rb │ │ ├── clean_spec.rb │ │ └── install_spec.rb │ ├── mock │ │ ├── source │ │ │ └── mock_spec.rb │ │ └── environment_spec.rb │ ├── source │ │ └── git_spec.rb │ ├── manifest_spec.rb │ ├── lockfile_spec.rb │ ├── environment │ │ └── runtime_cache_spec.rb │ ├── algorithms_spec.rb │ ├── spec_change_set_spec.rb │ ├── lockfile │ │ └── parser_spec.rb │ ├── manifest_set_spec.rb │ ├── dependency_spec.rb │ ├── dsl_spec.rb │ ├── environment_spec.rb │ └── resolver_spec.rb ├── support │ ├── project_path_macro.rb │ ├── with_env_macro.rb │ ├── method_patch_macro.rb │ └── fakefs.rb └── functional │ ├── cli_spec.rb │ ├── posix_spec.rb │ └── source │ ├── git_spec.rb │ └── git │ └── repository_spec.rb ├── Rakefile ├── librarian.gemspec ├── LICENSE.txt └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.2 2 | -------------------------------------------------------------------------------- /lib/librarian/mock.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/mock/extension' 2 | -------------------------------------------------------------------------------- /lib/librarian/mock/source.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/mock/source/mock' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | tmp 6 | vendor 7 | -------------------------------------------------------------------------------- /lib/librarian/error.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | class Error < StandardError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/librarian/source.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/source/git' 2 | require 'librarian/source/path' 3 | -------------------------------------------------------------------------------- /lib/librarian/mock/version.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | module Mock 3 | VERSION = "0.1.2" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/librarian/config.rb: -------------------------------------------------------------------------------- 1 | require "librarian/config/database" 2 | 3 | module Librarian 4 | module Config 5 | 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/librarian/version.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | VERSION = File.read(File.expand_path("../../../VERSION", __FILE__)).strip 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in librarian.gemspec 4 | gemspec 5 | 6 | gem "fakefs" 7 | 8 | gem "rubysl", :platforms => %w[rbx] 9 | -------------------------------------------------------------------------------- /lib/librarian/mock/extension.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/mock/environment' 2 | 3 | module Librarian 4 | module Mock 5 | extend self 6 | extend Librarian 7 | 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/librarian/action.rb: -------------------------------------------------------------------------------- 1 | require "librarian/action/clean" 2 | require "librarian/action/ensure" 3 | require "librarian/action/install" 4 | require "librarian/action/resolve" 5 | require "librarian/action/update" 6 | -------------------------------------------------------------------------------- /lib/librarian.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/version' 2 | require 'librarian/environment' 3 | 4 | module Librarian 5 | extend self 6 | 7 | def environment_class 8 | self::Environment 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | script: rspec spec -b 3 | rvm: 4 | - 1.8.7 5 | - 1.9.2 6 | - 1.9.3 7 | - 2.0.0 8 | - rbx-2.2 9 | - jruby-18mode 10 | - jruby-19mode 11 | - ruby-head 12 | matrix: 13 | allow_failures: 14 | - rvm: ruby-head 15 | -------------------------------------------------------------------------------- /lib/librarian/spec.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | class Spec 3 | 4 | attr_accessor :sources, :dependencies 5 | private :sources=, :dependencies= 6 | 7 | def initialize(sources, dependencies) 8 | self.sources = sources 9 | self.dependencies = dependencies 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/librarian/mock/dsl.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/dsl' 2 | require 'librarian/mock/source' 3 | 4 | module Librarian 5 | module Mock 6 | class Dsl < Librarian::Dsl 7 | 8 | dependency :dep 9 | 10 | source :src => Source::Mock 11 | 12 | shortcut :a, :src => 'source-a' 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/manifest/version_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/manifest" 2 | 3 | describe Librarian::Manifest::Version do 4 | 5 | describe "#inspect" do 6 | subject(:version) { described_class.new("3.2.1") } 7 | 8 | specify { expect(version.inspect).to eq "#" } 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/project_path_macro.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | module ProjectPathMacro 3 | 4 | project_path = Pathname.new(__FILE__).expand_path 5 | project_path = project_path.dirname until project_path.join("Rakefile").exist? 6 | 7 | PROJECT_PATH = project_path 8 | 9 | def project_path 10 | PROJECT_PATH 11 | end 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/unit/dependency/requirement_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/dependency" 2 | 3 | describe Librarian::Dependency::Requirement do 4 | 5 | describe "#inspect" do 6 | subject(:requirement) { described_class.new(">= 3.2.1") } 7 | 8 | specify { expect(requirement.inspect). 9 | to eq "#= 3.2.1>" } 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /lib/librarian/mock/cli.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/cli' 2 | require 'librarian/mock' 3 | 4 | module Librarian 5 | module Mock 6 | class Cli < Librarian::Cli 7 | 8 | module Particularity 9 | def root_module 10 | Mock 11 | end 12 | end 13 | 14 | include Particularity 15 | extend Particularity 16 | 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/with_env_macro.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | module WithEnvMacro 3 | 4 | module ClassMethods 5 | 6 | def with_env(new) 7 | old = Hash[new.map{|k, v| [k, ENV[k]]}] 8 | 9 | before { ENV.update(new) } 10 | after { ENV.update(old) } 11 | end 12 | 13 | end 14 | 15 | private 16 | 17 | def self.included(base) 18 | base.extend(ClassMethods) 19 | end 20 | 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/unit/action/base_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/action/base" 2 | 3 | module Librarian 4 | describe Action::Base do 5 | 6 | let(:env) { double } 7 | let(:action) { described_class.new(env) } 8 | 9 | subject { action } 10 | 11 | it { should respond_to :environment } 12 | 13 | it "should have the environment that was assigned to it" do 14 | expect(action.environment).to be env 15 | end 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/librarian/specfile.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | 3 | module Librarian 4 | class Specfile 5 | 6 | attr_accessor :environment, :path 7 | private :environment=, :path= 8 | 9 | def initialize(environment, path) 10 | self.environment = environment 11 | self.path = Pathname(path) 12 | end 13 | 14 | def read(precache_sources = []) 15 | environment.dsl(path, precache_sources) 16 | end 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/unit/mock/source/mock_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/mock" 2 | 3 | module Librarian 4 | module Mock 5 | module Source 6 | describe Mock do 7 | 8 | let(:env) { Librarian::Mock::Environment.new } 9 | 10 | describe ".new" do 11 | 12 | let(:source) { described_class.new(env, "source-a", {}) } 13 | subject { source } 14 | 15 | its(:environment) { should_not be_nil } 16 | 17 | end 18 | 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/librarian/mock/environment.rb: -------------------------------------------------------------------------------- 1 | require "librarian/environment" 2 | require "librarian/mock/dsl" 3 | require "librarian/mock/version" 4 | 5 | module Librarian 6 | module Mock 7 | class Environment < Environment 8 | 9 | def install_path 10 | nil 11 | end 12 | 13 | def registry(options = nil, &block) 14 | @registry ||= Source::Mock::Registry.new 15 | @registry.merge!(options, &block) 16 | @registry 17 | end 18 | 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/librarian/action/ensure.rb: -------------------------------------------------------------------------------- 1 | require "librarian/error" 2 | require "librarian/action/base" 3 | 4 | module Librarian 5 | module Action 6 | class Ensure < Base 7 | 8 | def run 9 | raise Error, "Cannot find #{specfile_name}!" unless project_path 10 | end 11 | 12 | private 13 | 14 | def specfile_name 15 | environment.specfile_name 16 | end 17 | 18 | def project_path 19 | environment.project_path 20 | end 21 | 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/librarian/action/base.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | module Action 3 | class Base 4 | 5 | attr_accessor :environment 6 | private :environment= 7 | 8 | attr_accessor :options 9 | private :options= 10 | 11 | def initialize(environment, options = { }) 12 | self.environment = environment 13 | self.options = options 14 | end 15 | 16 | private 17 | 18 | def debug(*args, &block) 19 | environment.logger.debug(*args, &block) 20 | end 21 | 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/librarian/support/abstract_method.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | module Support 3 | module AbstractMethod 4 | 5 | class << self 6 | def included(base) 7 | base.extend ClassMethods 8 | end 9 | end 10 | 11 | module ClassMethods 12 | def abstract_method(*names) 13 | names.reject{|name| respond_to?(name)}.each do |name, *args| 14 | define_method(name) { raise Exception, "Method #{self.class.name}##{name} is abstract!" } 15 | end 16 | end 17 | end 18 | 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/librarian/lockfile.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/lockfile/compiler' 2 | require 'librarian/lockfile/parser' 3 | 4 | module Librarian 5 | class Lockfile 6 | 7 | attr_accessor :environment 8 | private :environment= 9 | attr_reader :path 10 | 11 | def initialize(environment, path) 12 | self.environment = environment 13 | @path = path 14 | end 15 | 16 | def save(resolution) 17 | Compiler.new(environment).compile(resolution) 18 | end 19 | 20 | def load(string) 21 | Parser.new(environment).parse(string) 22 | end 23 | 24 | def read 25 | load(path.read) 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/functional/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require "securerandom" 2 | 3 | require "librarian/rspec/support/cli_macro" 4 | 5 | require "librarian/mock/cli" 6 | 7 | module Librarian 8 | module Mock 9 | describe Cli do 10 | include Librarian::RSpec::Support::CliMacro 11 | 12 | describe "version" do 13 | before do 14 | cli! "version" 15 | end 16 | 17 | it "should print the version" do 18 | expect(stdout).to eq strip_heredoc(<<-STDOUT) 19 | librarian-#{Librarian::VERSION} 20 | librarian-mock-#{Librarian::Mock::VERSION} 21 | STDOUT 22 | end 23 | end 24 | 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/unit/mock/environment_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/mock/environment" 2 | 3 | module Librarian::Mock 4 | describe Environment do 5 | 6 | let(:env) { described_class.new } 7 | 8 | describe "#version" do 9 | specify { expect(env.version).to eq Librarian::VERSION } 10 | end 11 | 12 | describe "#adapter_module" do 13 | specify { expect(env.adapter_module).to eq Librarian::Mock } 14 | end 15 | 16 | describe "#adapter_name" do 17 | specify { expect(env.adapter_name).to eq "mock" } 18 | end 19 | 20 | describe "#adapter_version" do 21 | specify { expect(env.adapter_version).to eq Librarian::Mock::VERSION } 22 | end 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/librarian/config/hash_source.rb: -------------------------------------------------------------------------------- 1 | require "librarian/source" 2 | 3 | module Librarian 4 | module Config 5 | class HashSource < Source 6 | 7 | attr_accessor :name, :raw 8 | private :name=, :raw= 9 | 10 | def initialize(adapter_name, options = { }) 11 | super 12 | 13 | self.name = options.delete(:name) or raise ArgumentError, "must provide name" 14 | self.raw = options.delete(:raw) or raise ArgumentError, "must provide raw" 15 | end 16 | 17 | def to_s 18 | name 19 | end 20 | 21 | private 22 | 23 | def load 24 | translate_raw_to_config(raw) 25 | end 26 | 27 | def save(config) 28 | raise Error, "nonsense!" 29 | end 30 | 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/support/method_patch_macro.rb: -------------------------------------------------------------------------------- 1 | require "securerandom" 2 | 3 | module Support 4 | module MethodPatchMacro 5 | 6 | def self.included(base) 7 | base.extend ClassMethods 8 | end 9 | 10 | module ClassMethods 11 | def with_module_method(mod, meth, &block) 12 | tag = SecureRandom.hex(8) 13 | orig_meth = "_#{tag}_#{meth}".to_sym 14 | before do 15 | mod.module_eval do 16 | alias_method orig_meth, meth 17 | define_method meth, &block 18 | end 19 | end 20 | after do 21 | mod.module_eval do 22 | alias_method meth, orig_meth 23 | remove_method orig_meth 24 | end 25 | end 26 | end 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | require 'rspec/core/rake_task' 3 | 4 | module Bundler 5 | class GemHelper 6 | 7 | def build_gem_with_built_spec 8 | spec = Gem::Specification.load(spec_path) 9 | spec_ruby = spec.to_ruby 10 | original_spec_path = spec_path + ".original" 11 | FileUtils.mv(spec_path, original_spec_path) 12 | File.open(spec_path, "wb"){|f| f.write(spec_ruby)} 13 | build_gem_without_built_spec 14 | ensure 15 | FileUtils.mv(original_spec_path, spec_path) 16 | end 17 | 18 | alias build_gem_without_built_spec build_gem 19 | alias build_gem build_gem_with_built_spec 20 | 21 | end 22 | end 23 | 24 | Bundler::GemHelper.install_tasks 25 | 26 | RSpec::Core::RakeTask.new('spec') 27 | task :default => :spec 28 | 29 | -------------------------------------------------------------------------------- /lib/librarian/helpers.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | 3 | # PRIVATE 4 | # 5 | # Adapters must not rely on these methods since they will change. 6 | # 7 | # Adapters requiring similar methods ought to re-implement them. 8 | module Helpers 9 | extend self 10 | 11 | # [active_support/core_ext/string/strip] 12 | def strip_heredoc(string) 13 | indent = string.scan(/^[ \t]*(?=\S)/).min 14 | indent = indent.respond_to?(:size) ? indent.size : 0 15 | string.gsub(/^[ \t]{#{indent}}/, '') 16 | end 17 | 18 | # [active_support/inflector/methods] 19 | def camel_cased_to_dasherized(camel_cased_word) 20 | word = camel_cased_word.to_s.dup 21 | word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1-\2') 22 | word.gsub!(/([a-z\d])([A-Z])/,'\1-\2') 23 | word.downcase! 24 | word 25 | end 26 | 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /spec/unit/source/git_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian" 2 | 3 | module Librarian 4 | module Source 5 | describe Git do 6 | 7 | let(:env) { Environment.new } 8 | 9 | describe "validating options for the specfile" do 10 | 11 | context "with only known options" do 12 | it "should not raise" do 13 | expect { described_class.from_spec_args(env, "some://git/repo.git", :ref => "megapatches") }. 14 | to_not raise_error 15 | end 16 | end 17 | 18 | context "with an unknown option" do 19 | it "should raise" do 20 | expect { described_class.from_spec_args(env, "some://git/repo.git", :branch => "megapatches") }. 21 | to raise_error Error, "unrecognized options: branch" 22 | end 23 | end 24 | 25 | end 26 | 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/functional/posix_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/posix" 2 | 3 | require "support/project_path_macro" 4 | 5 | describe Librarian::Posix do 6 | include Support::ProjectPathMacro 7 | 8 | let(:tmp_path) { project_path + "tmp/spec/functional/posix" } 9 | after { tmp_path.rmtree if tmp_path && tmp_path.exist? } 10 | 11 | describe ".run!" do 12 | 13 | it "returns the stdout" do 14 | res = described_class.run!(%w[echo hello there]).strip 15 | expect(res).to eq "hello there" 16 | end 17 | 18 | it "changes directory" do 19 | tmp_path.mkpath 20 | res = described_class.run!(%w[pwd], :chdir => tmp_path).strip 21 | expect(res).to eq tmp_path.to_s 22 | end 23 | 24 | it "reads the env" do 25 | res = described_class.run!(%w[env], :env => {"KOALA" => "BEAR"}) 26 | line = res.lines.find{|l| l.start_with?("KOALA=")}.strip 27 | expect(line).to eq "KOALA=BEAR" 28 | end 29 | 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /spec/unit/action/ensure_spec.rb: -------------------------------------------------------------------------------- 1 | require "tmpdir" 2 | 3 | require "librarian/error" 4 | require "librarian/action/ensure" 5 | 6 | module Librarian 7 | describe Action::Ensure do 8 | 9 | let(:env) { double } 10 | let(:action) { described_class.new(env) } 11 | 12 | before do 13 | env.stub(:specfile_name) { "Specfile" } 14 | end 15 | 16 | describe "#run" do 17 | 18 | context "when the environment does not know its project path" do 19 | before { env.stub(:project_path) { nil } } 20 | 21 | it "should raise an error describing that the specfile is mising" do 22 | expect { action.run }.to raise_error(Error, "Cannot find Specfile!") 23 | end 24 | end 25 | 26 | context "when the environment knows its project path" do 27 | before { env.stub(:project_path) { Dir.tmpdir } } 28 | 29 | it "should not raise an error" do 30 | expect { action.run }.to_not raise_error 31 | end 32 | end 33 | 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/librarian/action/clean.rb: -------------------------------------------------------------------------------- 1 | require "librarian/action/base" 2 | 3 | module Librarian 4 | module Action 5 | class Clean < Base 6 | 7 | def run 8 | clean_cache_path 9 | clean_install_path 10 | end 11 | 12 | private 13 | 14 | def clean_cache_path 15 | if cache_path.exist? 16 | debug { "Deleting #{project_relative_path_to(cache_path)}" } 17 | cache_path.rmtree 18 | end 19 | end 20 | 21 | def clean_install_path 22 | if install_path.exist? 23 | install_path.children.each do |c| 24 | debug { "Deleting #{project_relative_path_to(c)}" } 25 | c.rmtree unless c.file? 26 | end 27 | end 28 | end 29 | 30 | def cache_path 31 | environment.cache_path 32 | end 33 | 34 | def install_path 35 | environment.install_path 36 | end 37 | 38 | def project_relative_path_to(path) 39 | environment.project_relative_path_to(path) 40 | end 41 | 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/unit/manifest_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/manifest" 2 | 3 | describe Librarian::Manifest do 4 | 5 | describe "validations" do 6 | 7 | context "when the name is blank" do 8 | it "raises" do 9 | expect { described_class.new(nil, "") }. 10 | to raise_error(ArgumentError, %{name ("") must be sensible}) 11 | end 12 | end 13 | 14 | context "when the name has leading whitespace" do 15 | it "raises" do 16 | expect { described_class.new(nil, " the-name") }. 17 | to raise_error(ArgumentError, %{name (" the-name") must be sensible}) 18 | end 19 | end 20 | 21 | context "when the name has trailing whitespace" do 22 | it "raises" do 23 | expect { described_class.new(nil, "the-name ") }. 24 | to raise_error(ArgumentError, %{name ("the-name ") must be sensible}) 25 | end 26 | end 27 | 28 | context "when the name is a single character" do 29 | it "passes" do 30 | described_class.new(nil, "R") 31 | end 32 | end 33 | 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /spec/support/fakefs.rb: -------------------------------------------------------------------------------- 1 | require "fakefs/safe" 2 | require "fakefs/spec_helpers" 3 | require "support/method_patch_macro" 4 | 5 | if defined?(Rubinius) 6 | module Rubinius 7 | class CodeLoader 8 | class << self 9 | alias_method :require_fakefs_original, :require 10 | def require(s) 11 | ::FakeFS.without { require_fakefs_original(s) } 12 | end 13 | end 14 | end 15 | end 16 | end 17 | 18 | module Support 19 | module FakeFS 20 | 21 | def self.included(base) 22 | base.module_exec do 23 | include ::FakeFS::SpecHelpers 24 | end 25 | 26 | # Since ruby-1.9.3-p286, Kernel#Pathname was changed in a way that broke 27 | # FakeFS's assumptions. It used to lookup the Pathname constant (which is 28 | # where FakeFS hooks) and send it #new, but now it keeps a reference to 29 | # the Pathname constant (breaking the FakeFS hook). 30 | base.module_exec do 31 | include MethodPatchMacro 32 | with_module_method(Kernel, :Pathname){|s| Pathname.new(s)} 33 | end 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /librarian.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = "librarian" 7 | gem.version = File.read(File.expand_path("../VERSION", __FILE__)).strip 8 | gem.authors = ["Jay Feldblum"] 9 | gem.email = ["y_feldblum@yahoo.com"] 10 | gem.summary = %q{A Framework for Bundlers.} 11 | gem.description = %q{A Framework for Bundlers.} 12 | gem.homepage = "https://github.com/applicationsonline/librarian" 13 | gem.license = "MIT" 14 | 15 | gem.files = `git ls-files`.split($/) 16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | 20 | gem.add_dependency "thor", "~> 0.15" 21 | gem.add_dependency "highline" 22 | 23 | gem.add_development_dependency "rake" 24 | gem.add_development_dependency "rspec" 25 | gem.add_development_dependency "json" 26 | gem.add_development_dependency "fakefs", "~> 0.4.2" 27 | end 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 ApplicationsOnline, LLC. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/librarian/source/path.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/source/basic_api' 2 | require 'librarian/source/local' 3 | 4 | module Librarian 5 | module Source 6 | class Path 7 | include BasicApi 8 | include Local 9 | 10 | lock_name 'PATH' 11 | spec_options [] 12 | 13 | attr_accessor :environment 14 | private :environment= 15 | attr_reader :path 16 | 17 | def initialize(environment, path, options) 18 | self.environment = environment 19 | @path = path 20 | end 21 | 22 | def to_s 23 | path.to_s 24 | end 25 | 26 | def ==(other) 27 | other && 28 | self.class == other.class && 29 | self.path == other.path 30 | end 31 | 32 | def to_spec_args 33 | [path.to_s, {}] 34 | end 35 | 36 | def to_lock_options 37 | {:remote => path} 38 | end 39 | 40 | def pinned? 41 | false 42 | end 43 | 44 | def unpin! 45 | end 46 | 47 | def cache! 48 | end 49 | 50 | def filesystem_path 51 | @filesystem_path ||= Pathname.new(path).expand_path(environment.project_path) 52 | end 53 | 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/librarian/config/file_source.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | 3 | require "librarian/config/source" 4 | 5 | module Librarian 6 | module Config 7 | class FileSource < Source 8 | 9 | attr_accessor :config_path 10 | private :config_path= 11 | 12 | def initialize(adapter_name, options = { }) 13 | super 14 | 15 | self.config_path = options.delete(:config_path) or raise ArgumentError, "must provide config_path" 16 | end 17 | 18 | def to_s 19 | config_path 20 | end 21 | 22 | private 23 | 24 | def load 25 | return { } unless File.file?(config_path) 26 | 27 | raw = YAML.load_file(config_path) 28 | return { } unless Hash === raw 29 | 30 | translate_raw_to_config(raw) 31 | end 32 | 33 | def save(config) 34 | raw = translate_config_to_raw(config) 35 | 36 | if config.empty? 37 | File.delete(config_path) if File.file?(config_path) 38 | else 39 | config_dir = File.dirname(config_path) 40 | FileUtils.mkpath(config_dir) unless File.directory?(config_dir) 41 | File.open(config_path, "wb"){|f| YAML.dump(raw, f)} 42 | end 43 | end 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/librarian/action/resolve.rb: -------------------------------------------------------------------------------- 1 | require "librarian/resolver" 2 | require "librarian/spec_change_set" 3 | require "librarian/action/base" 4 | require "librarian/action/persist_resolution_mixin" 5 | 6 | module Librarian 7 | module Action 8 | class Resolve < Base 9 | include PersistResolutionMixin 10 | 11 | def run 12 | if force? || !lockfile_path.exist? 13 | spec = specfile.read 14 | manifests = [] 15 | else 16 | lock = lockfile.read 17 | spec = specfile.read(lock.sources) 18 | changes = spec_change_set(spec, lock) 19 | if changes.same? 20 | debug { "The specfile is unchanged: nothing to do." } 21 | return 22 | end 23 | manifests = changes.analyze 24 | end 25 | 26 | resolution = resolver.resolve(spec, manifests) 27 | persist_resolution(resolution) 28 | end 29 | 30 | private 31 | 32 | def force? 33 | options[:force] 34 | end 35 | 36 | def resolver 37 | Resolver.new(environment) 38 | end 39 | 40 | def spec_change_set(spec, lock) 41 | SpecChangeSet.new(environment, spec, lock) 42 | end 43 | 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/librarian/logger.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | class Logger 3 | 4 | librarian_path = Pathname(__FILE__) 5 | librarian_path = librarian_path.dirname until librarian_path.join("lib").directory? 6 | LIBRARIAN_PATH = librarian_path 7 | 8 | attr_accessor :environment 9 | private :environment= 10 | 11 | def initialize(environment) 12 | self.environment = environment 13 | end 14 | 15 | def info(string = nil, &block) 16 | return unless ui 17 | 18 | ui.info(string || yield) 19 | end 20 | 21 | def debug(string = nil, &block) 22 | return unless ui 23 | 24 | if ui.respond_to?(:debug_line_numbers) && ui.debug_line_numbers 25 | loc = caller.find{|l| !(l =~ /in `debug'$/)} 26 | if loc =~ /^(.+):(\d+):in `(.+)'$/ 27 | loc = "#{Pathname.new($1).relative_path_from(LIBRARIAN_PATH)}:#{$2}:in `#{$3}'" 28 | end 29 | ui.debug { "[Librarian] #{string || yield} [#{loc}]" } 30 | else 31 | ui.debug { "[Librarian] #{string || yield}" } 32 | end 33 | end 34 | 35 | def relative_path_to(path) 36 | environment.project_relative_path_to(path) 37 | end 38 | 39 | private 40 | 41 | def ui 42 | environment.ui 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/librarian/dsl/receiver.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | 3 | module Librarian 4 | class Dsl 5 | class Receiver 6 | 7 | def initialize(target) 8 | singleton_class = class << self; self end 9 | singleton_class.class_eval do 10 | define_method(target.dependency_name) do |*args, &block| 11 | target.dependency(*args, &block) 12 | end 13 | define_method(:source) do |*args, &block| 14 | target.source(*args, &block) 15 | end 16 | target.source_types.each do |source_type| 17 | name = source_type[0] 18 | define_method(name) do |*args, &block| 19 | target.source(name, *args, &block) 20 | end 21 | end 22 | end 23 | end 24 | 25 | def run(specfile = nil) 26 | specfile = Proc.new if block_given? 27 | 28 | case specfile 29 | when Pathname 30 | instance_eval(File.read(specfile), specfile.to_s, 1) 31 | when String 32 | instance_eval(specfile) 33 | when Proc 34 | instance_eval(&specfile) 35 | else 36 | raise ArgumentError, "specfile must be a #{Pathname}, #{String}, or #{Proc} if no block is given (it was #{specfile.inspect})" 37 | end 38 | end 39 | 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/librarian/source/basic_api.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | module Source 3 | module BasicApi 4 | 5 | def self.included(base) 6 | base.extend ClassMethods 7 | class << base 8 | def lock_name(name) 9 | def_sclass_prop(:lock_name, name) 10 | end 11 | 12 | def spec_options(keys) 13 | def_sclass_prop(:spec_options, keys) 14 | end 15 | 16 | private 17 | 18 | def def_sclass_prop(name, arg) 19 | sclass = class << self ; self ; end 20 | sclass.module_exec do 21 | remove_method(name) 22 | define_method(name) { arg } 23 | end 24 | end 25 | end 26 | end 27 | 28 | module ClassMethods 29 | def from_lock_options(environment, options) 30 | new(environment, options[:remote], options.reject{|k, v| k == :remote}) 31 | end 32 | 33 | def from_spec_args(environment, param, options) 34 | recognized_options = spec_options 35 | unrecognized_options = options.keys - recognized_options 36 | unrecognized_options.empty? or raise Error, 37 | "unrecognized options: #{unrecognized_options.join(", ")}" 38 | 39 | new(environment, param, options) 40 | end 41 | end 42 | 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/librarian/linter/source_linter.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | module Linter 3 | class SourceLinter 4 | 5 | class << self 6 | def lint!(klass) 7 | new(klass).lint! 8 | end 9 | end 10 | 11 | attr_accessor :klass 12 | private :klass= 13 | 14 | def initialize(klass) 15 | self.klass = klass 16 | end 17 | 18 | def lint! 19 | lint_class_responds_to! *[ 20 | :lock_name, 21 | :from_spec_args, 22 | :from_lock_options, 23 | ] 24 | 25 | lint_instance_responds_to! *[ 26 | :to_spec_args, 27 | :to_lock_options, 28 | :manifests, 29 | :fetch_version, 30 | :fetch_dependencies, 31 | :pinned?, 32 | :unpin!, 33 | :install!, 34 | ] 35 | end 36 | 37 | private 38 | 39 | def lint_class_responds_to!(*names) 40 | missing = names.reject{|name| klass.respond_to?(name)} 41 | return if missing.empty? 42 | 43 | raise "class must respond to #{missing.join(', ')}" 44 | end 45 | 46 | def lint_instance_responds_to!(*names) 47 | missing = names - klass.public_instance_methods.map(&:to_sym) 48 | return if missing.empty? 49 | 50 | raise "instance must respond to #{missing.join(', ')}" 51 | end 52 | 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/librarian/ui.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems/user_interaction' 2 | 3 | module Librarian 4 | class UI 5 | def warn(message = nil) 6 | end 7 | 8 | def debug(message = nil) 9 | end 10 | 11 | def error(message = nil) 12 | end 13 | 14 | def info(message = nil) 15 | end 16 | 17 | def confirm(message = nil) 18 | end 19 | 20 | class Shell < UI 21 | attr_writer :shell 22 | attr_reader :debug_line_numbers 23 | 24 | def initialize(shell) 25 | @shell = shell 26 | @quiet = false 27 | @debug = ENV['DEBUG'] 28 | @debug_line_numbers = false 29 | end 30 | 31 | def debug(message = nil) 32 | @shell.say(message || yield) if @debug && !@quiet 33 | end 34 | 35 | def info(message = nil) 36 | @shell.say(message || yield) if !@quiet 37 | end 38 | 39 | def confirm(message = nil) 40 | @shell.say(message || yield, :green) if !@quiet 41 | end 42 | 43 | def warn(message = nil) 44 | @shell.say(message || yield, :yellow) 45 | end 46 | 47 | def error(message = nil) 48 | @shell.say(message || yield, :red) 49 | end 50 | 51 | def be_quiet! 52 | @quiet = true 53 | end 54 | 55 | def debug! 56 | @debug = true 57 | end 58 | 59 | def debug_line_numbers! 60 | @debug_line_numbers = true 61 | end 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /lib/librarian/action/update.rb: -------------------------------------------------------------------------------- 1 | require "librarian/manifest_set" 2 | require "librarian/resolver" 3 | require "librarian/spec_change_set" 4 | require "librarian/action/base" 5 | require "librarian/action/persist_resolution_mixin" 6 | 7 | module Librarian 8 | module Action 9 | class Update < Base 10 | include PersistResolutionMixin 11 | 12 | def run 13 | unless lockfile_path.exist? 14 | raise Error, "Lockfile missing!" 15 | end 16 | previous_resolution = lockfile.load(lockfile_path.read) 17 | spec = specfile.read(previous_resolution.sources) 18 | changes = spec_change_set(spec, previous_resolution) 19 | manifests = changes.same? ? previous_resolution.manifests : changes.analyze 20 | partial_manifests = ManifestSet.deep_strip(manifests, dependency_names) 21 | unpinnable_sources = previous_resolution.sources - partial_manifests.map(&:source) 22 | unpinnable_sources.each(&:unpin!) 23 | 24 | resolution = resolver.resolve(spec, partial_manifests) 25 | persist_resolution(resolution) 26 | end 27 | 28 | private 29 | 30 | def dependency_names 31 | options[:names] 32 | end 33 | 34 | def resolver 35 | Resolver.new(environment) 36 | end 37 | 38 | def spec_change_set(spec, lock) 39 | SpecChangeSet.new(environment, spec, lock) 40 | end 41 | 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/librarian/action/persist_resolution_mixin.rb: -------------------------------------------------------------------------------- 1 | require "librarian/error" 2 | require "librarian/spec_change_set" 3 | 4 | module Librarian 5 | module Action 6 | module PersistResolutionMixin 7 | 8 | private 9 | 10 | def persist_resolution(resolution) 11 | resolution && resolution.correct? or raise Error, 12 | "Could not resolve the dependencies." 13 | 14 | lockfile_text = lockfile.save(resolution) 15 | debug { "Bouncing #{lockfile_name}" } 16 | bounced_lockfile_text = lockfile.save(lockfile.load(lockfile_text)) 17 | unless bounced_lockfile_text == lockfile_text 18 | debug { "lockfile_text: \n#{lockfile_text}" } 19 | debug { "bounced_lockfile_text: \n#{bounced_lockfile_text}" } 20 | raise Error, "Cannot bounce #{lockfile_name}!" 21 | end 22 | lockfile_path.open('wb') { |f| f.write(lockfile_text) } 23 | end 24 | 25 | def specfile_name 26 | environment.specfile_name 27 | end 28 | 29 | def lockfile_name 30 | environment.lockfile_name 31 | end 32 | 33 | def specfile_path 34 | environment.specfile_path 35 | end 36 | 37 | def lockfile_path 38 | environment.lockfile_path 39 | end 40 | 41 | def specfile 42 | environment.specfile 43 | end 44 | 45 | def lockfile 46 | environment.lockfile 47 | end 48 | 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/librarian/source/local.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/support/abstract_method' 2 | 3 | module Librarian 4 | module Source 5 | # Requires that the including source class have methods: 6 | # #path 7 | # #environment 8 | module Local 9 | 10 | include Support::AbstractMethod 11 | 12 | abstract_method :path, :fetch_version, :fetch_dependencies 13 | 14 | def manifests(name) 15 | manifest = Manifest.new(self, name) 16 | [manifest].compact 17 | end 18 | 19 | def manifest_search_paths(name) 20 | @manifest_search_paths ||= { } 21 | @manifest_search_paths[name] ||= begin 22 | cache! 23 | paths = [filesystem_path, filesystem_path.join(name)] 24 | paths.select{|s| s.exist?} 25 | end 26 | end 27 | 28 | def found_path(name) 29 | @_found_paths ||= { } 30 | @_found_paths[name] ||= begin 31 | paths = manifest_search_paths(name) 32 | paths.find{|p| manifest?(name, p)} 33 | end 34 | end 35 | 36 | private 37 | 38 | abstract_method :manifest? # (name, path) -> boolean 39 | 40 | def info(*args, &block) 41 | environment.logger.info(*args, &block) 42 | end 43 | 44 | def debug(*args, &block) 45 | environment.logger.debug(*args, &block) 46 | end 47 | 48 | def relative_path_to(path) 49 | environment.logger.relative_path_to(path) 50 | end 51 | 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/librarian/resolution.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | # 3 | # Represents the output of the resolution process. Captures the declared 4 | # dependencies plus the full set of resolved manifests. The sources are 5 | # already known by the dependencies and by the resolved manifests, so they do 6 | # not need to be captured explicitly. 7 | # 8 | # This representation may be produced by the resolver, may be serialized into 9 | # a lockfile, and may be deserialized from a lockfile. It is expected that the 10 | # lockfile is a direct representation in text of this representation, so that 11 | # the serialization-deserialization process is just the identity function. 12 | # 13 | class Resolution 14 | attr_accessor :dependencies, :manifests, :manifests_index 15 | private :dependencies=, :manifests=, :manifests_index= 16 | 17 | def initialize(dependencies, manifests) 18 | self.dependencies = dependencies 19 | self.manifests = manifests 20 | self.manifests_index = build_manifests_index(manifests) 21 | end 22 | 23 | def correct? 24 | manifests && manifests_consistent_with_dependencies? && manifests_internally_consistent? 25 | end 26 | 27 | def sources 28 | manifests.map(&:source).uniq 29 | end 30 | 31 | private 32 | 33 | def build_manifests_index(manifests) 34 | Hash[manifests.map{|m| [m.name, m]}] if manifests 35 | end 36 | 37 | def manifests_consistent_with_dependencies? 38 | ManifestSet.new(manifests).in_compliance_with?(dependencies) 39 | end 40 | 41 | def manifests_internally_consistent? 42 | ManifestSet.new(manifests).consistent? 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Librarian [![Build Status](https://secure.travis-ci.org/applicationsonline/librarian.png)](http://travis-ci.org/applicationsonline/librarian) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/applicationsonline/librarian) 2 | ========= 3 | 4 | Librarian is a framework for writing bundlers, which are tools that resolve, 5 | fetch, install, and isolate a project's dependencies, in Ruby. 6 | 7 | A bundler written with Librarian will expect you to provide a specfile listing 8 | your project's declared dependencies, including any version constraints and 9 | including the upstream sources for finding them. Librarian can resolve the spec, 10 | write a lockfile listing the full resolution, fetch the resolved dependencies, 11 | install them, and isolate them in your project. 12 | 13 | A bundler written with Librarian will be similar in kind to [Bundler](http://gembundler.com), 14 | the bundler for Ruby gems that many modern Rails applications use. 15 | 16 | How to Contribute 17 | ----------------- 18 | 19 | ### Running the tests 20 | 21 | Ensure the gem dependencies are installed: 22 | 23 | $ bundle 24 | 25 | Run the tests with the default rake task: 26 | 27 | $ [bundle exec] rake 28 | 29 | or directly with the rspec command: 30 | 31 | $ [bundle exec] rspec spec 32 | 33 | ### Installing locally 34 | 35 | Ensure the gem dependencies are installed: 36 | 37 | $ bundle 38 | 39 | Install from the repository: 40 | 41 | $ [bundle exec] rake install 42 | 43 | ### Reporting Issues 44 | 45 | Please include a reproducible test case. 46 | 47 | License 48 | ------- 49 | 50 | Written by Jay Feldblum. 51 | 52 | Copyright (c) 2011-2013 ApplicationsOnline, LLC. 53 | 54 | Released under the terms of the MIT License. For further information, please see 55 | the file `LICENSE.txt`. 56 | -------------------------------------------------------------------------------- /spec/unit/lockfile_spec.rb: -------------------------------------------------------------------------------- 1 | require 'librarian' 2 | require 'librarian/mock' 3 | 4 | module Librarian 5 | describe Lockfile do 6 | 7 | let(:env) { Mock::Environment.new } 8 | 9 | before do 10 | env.registry :clear => true do 11 | source 'source-1' do 12 | spec 'alpha', '1.1' 13 | end 14 | end 15 | end 16 | 17 | let(:spec) do 18 | env.dsl do 19 | src 'source-1' 20 | dep 'alpha', '1.1' 21 | end 22 | end 23 | 24 | let(:resolver) { env.resolver } 25 | let(:resolution) { resolver.resolve(spec) } 26 | 27 | context "sanity" do 28 | context "the resolution" do 29 | subject { resolution } 30 | 31 | it { should be_correct } 32 | it { should have(1).manifests } 33 | end 34 | end 35 | 36 | describe "#save" do 37 | let(:lockfile) { env.ephemeral_lockfile } 38 | let(:lockfile_text) { lockfile.save(resolution) } 39 | 40 | context "just saving" do 41 | it "should return the lockfile text" do 42 | expect(lockfile_text).to_not be_nil 43 | end 44 | end 45 | 46 | context "saving and reloading" do 47 | let(:reloaded_resolution) { lockfile.load(lockfile_text) } 48 | 49 | it "should have the expected manifests" do 50 | expect(reloaded_resolution.manifests.count).to eq resolution.manifests.count 51 | end 52 | end 53 | 54 | context "bouncing" do 55 | let(:bounced_resolution) { lockfile.load(lockfile_text) } 56 | let(:bounced_lockfile_text) { lockfile.save(bounced_resolution) } 57 | 58 | it "should return the same lockfile text after bouncing as before bouncing" do 59 | expect(bounced_lockfile_text).to eq lockfile_text 60 | end 61 | end 62 | end 63 | 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/librarian/mock/source/mock.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/manifest' 2 | require 'librarian/source/basic_api' 3 | require 'librarian/mock/source/mock/registry' 4 | 5 | module Librarian 6 | module Mock 7 | module Source 8 | class Mock 9 | include Librarian::Source::BasicApi 10 | 11 | lock_name 'MOCK' 12 | spec_options [] 13 | 14 | attr_accessor :environment 15 | private :environment= 16 | attr_reader :name 17 | 18 | def initialize(environment, name, options) 19 | self.environment = environment 20 | @name = name 21 | end 22 | 23 | def to_s 24 | name 25 | end 26 | 27 | def ==(other) 28 | other && 29 | self.class == other.class && 30 | self.name == other.name 31 | end 32 | 33 | def to_spec_args 34 | [name, {}] 35 | end 36 | 37 | def to_lock_options 38 | {:remote => name} 39 | end 40 | 41 | def registry 42 | environment.registry[name] 43 | end 44 | 45 | def manifest(name, version, dependencies) 46 | manifest = Manifest.new(self, name) 47 | manifest.version = version 48 | manifest.dependencies = dependencies 49 | manifest 50 | end 51 | 52 | def manifests(name) 53 | if d = registry[name] 54 | d.map{|v| manifest(name, v[:version], v[:dependencies])} 55 | else 56 | nil 57 | end 58 | end 59 | 60 | def install!(manifest) 61 | end 62 | 63 | def to_s 64 | name 65 | end 66 | 67 | def fetch_version(name, extra) 68 | extra 69 | end 70 | 71 | def fetch_dependencies(name, version, extra) 72 | d = registry[name] 73 | m = d.find{|v| v[:version] == version.to_s} 74 | m[:dependencies] 75 | end 76 | 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/librarian/lockfile/compiler.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | class Lockfile 3 | class Compiler 4 | 5 | attr_accessor :environment 6 | private :environment= 7 | 8 | def initialize(environment) 9 | self.environment = environment 10 | end 11 | 12 | def compile(resolution) 13 | out = StringIO.new 14 | save_sources(out, resolution.manifests) 15 | save_dependencies(out, resolution.dependencies) 16 | out.string 17 | end 18 | 19 | private 20 | 21 | def save_sources(out, manifests) 22 | dsl_class.source_types.map{|t| t[1]}.each do |type| 23 | type_manifests = manifests.select{|m| type === m.source} 24 | sources = type_manifests.map{|m| m.source}.uniq.sort_by{|s| s.to_s} 25 | sources.each do |source| 26 | source_manifests = type_manifests.select{|m| source == m.source} 27 | save_source(out, source, source_manifests) 28 | end 29 | end 30 | end 31 | 32 | def save_source(out, source, manifests) 33 | out.puts "#{source.class.lock_name}" 34 | options = source.to_lock_options 35 | remote = options.delete(:remote) 36 | out.puts " remote: #{remote}" 37 | options.to_a.sort_by{|a| a[0].to_s}.each do |o| 38 | out.puts " #{o[0]}: #{o[1]}" 39 | end 40 | out.puts " specs:" 41 | manifests.sort_by{|a| a.name}.each do |manifest| 42 | out.puts " #{manifest.name} (#{manifest.version})" 43 | manifest.dependencies.sort_by{|a| a.name}.each do |dependency| 44 | out.puts " #{dependency.name} (#{dependency.requirement})" 45 | end 46 | end 47 | out.puts "" 48 | end 49 | 50 | def save_dependencies(out, dependencies) 51 | out.puts "DEPENDENCIES" 52 | dependencies.sort_by{|a| a.name}.each do |d| 53 | res = "#{d.name}" 54 | res << " (#{d.requirement})" if d.requirement 55 | out.puts " #{res}" 56 | end 57 | out.puts "" 58 | end 59 | 60 | def dsl_class 61 | environment.dsl_class 62 | end 63 | 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/librarian/action/install.rb: -------------------------------------------------------------------------------- 1 | require "librarian/manifest_set" 2 | require "librarian/spec_change_set" 3 | require "librarian/action/base" 4 | 5 | module Librarian 6 | module Action 7 | class Install < Base 8 | 9 | def run 10 | check_preconditions 11 | 12 | perform_installation 13 | end 14 | 15 | private 16 | 17 | def check_preconditions 18 | check_specfile 19 | check_lockfile 20 | check_consistent 21 | end 22 | 23 | def check_specfile 24 | raise Error, "#{specfile_name} missing!" unless specfile_path.exist? 25 | end 26 | 27 | def check_lockfile 28 | raise Error, "#{lockfile_name} missing!" unless lockfile_path.exist? 29 | end 30 | 31 | def check_consistent 32 | raise Error, "#{specfile_name} and #{lockfile_name} are out of sync!" unless spec_consistent_with_lock? 33 | end 34 | 35 | def perform_installation 36 | manifests = sorted_manifests 37 | 38 | create_install_path 39 | install_manifests(manifests) 40 | end 41 | 42 | def create_install_path 43 | install_path.rmtree if install_path.exist? 44 | install_path.mkpath 45 | end 46 | 47 | def install_manifests(manifests) 48 | manifests.each do |manifest| 49 | manifest.install! 50 | end 51 | end 52 | 53 | def sorted_manifests 54 | ManifestSet.sort(lock.manifests) 55 | end 56 | 57 | def specfile_name 58 | environment.specfile_name 59 | end 60 | 61 | def specfile_path 62 | environment.specfile_path 63 | end 64 | 65 | def lockfile_name 66 | environment.lockfile_name 67 | end 68 | 69 | def lockfile_path 70 | environment.lockfile_path 71 | end 72 | 73 | def spec 74 | environment.spec 75 | end 76 | 77 | def lock 78 | environment.lock 79 | end 80 | 81 | def spec_change_set(spec, lock) 82 | SpecChangeSet.new(environment, spec, lock) 83 | end 84 | 85 | def spec_consistent_with_lock? 86 | spec_change_set(spec, lock).same? 87 | end 88 | 89 | def install_path 90 | environment.install_path 91 | end 92 | 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/unit/environment/runtime_cache_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/environment/runtime_cache" 2 | 3 | module Librarian 4 | class Environment 5 | describe RuntimeCache do 6 | 7 | let(:rtc) { described_class.new } 8 | let(:key) { ["brah", "nick"] } 9 | let(:key_x) { ["brah", "phar"] } 10 | let(:key_y) { ["rost", "phar"] } 11 | 12 | def triple(keypair) 13 | [rtc.include?(*keypair), rtc.get(*keypair), rtc.memo(*keypair){yield}] 14 | end 15 | 16 | context "originally" do 17 | specify { expect(triple(key){9}).to eql([false, nil, 9]) } 18 | end 19 | 20 | context "after put" do 21 | before { rtc.put(*key){6} } 22 | 23 | specify { expect(triple(key){9}).to eql([true, 6, 6]) } 24 | specify { expect(triple(key_x){9}).to eql([false, nil, 9]) } 25 | specify { expect(triple(key_y){9}).to eql([false, nil, 9]) } 26 | end 27 | 28 | context "after put then delete" do 29 | before { rtc.put(*key){6} } 30 | before { rtc.delete *key } 31 | 32 | specify { expect(triple(key){9}).to eql([false, nil, 9]) } 33 | specify { expect(triple(key_x){9}).to eql([false, nil, 9]) } 34 | specify { expect(triple(key_y){9}).to eql([false, nil, 9]) } 35 | end 36 | 37 | context "after memo" do 38 | before { rtc.memo(*key){6} } 39 | 40 | specify { expect(triple(key){9}).to eql([true, 6, 6]) } 41 | specify { expect(triple(key_x){9}).to eql([false, nil, 9]) } 42 | specify { expect(triple(key_y){9}).to eql([false, nil, 9]) } 43 | end 44 | 45 | context "after memo then delete" do 46 | before { rtc.memo(*key){6} } 47 | before { rtc.delete *key } 48 | 49 | specify { expect(triple(key){9}).to eql([false, nil, 9]) } 50 | specify { expect(triple(key_x){9}).to eql([false, nil, 9]) } 51 | specify { expect(triple(key_y){9}).to eql([false, nil, 9]) } 52 | end 53 | 54 | context "with keyspace wrapper" do 55 | let(:krtc) { rtc.keyspace("brah") } 56 | let(:key) { "nick" } 57 | let(:key_x) { "phar" } 58 | 59 | def triple(keypair) 60 | [krtc.include?(key), krtc.get(key), krtc.memo(key){yield}] 61 | end 62 | 63 | context "after put" do 64 | before { krtc.put(key){6} } 65 | 66 | specify { expect(triple(key){9}).to eql([true, 6, 6]) } 67 | end 68 | 69 | end 70 | 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/librarian/mock/source/mock/registry.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | module Mock 3 | module Source 4 | class Mock 5 | class Registry 6 | 7 | module Dsl 8 | 9 | class Top 10 | def initialize(sources) 11 | @sources = sources 12 | end 13 | def source(name, &block) 14 | @sources[name] ||= {} 15 | Source.new(@sources[name]).instance_eval(&block) if block 16 | end 17 | end 18 | 19 | class Source 20 | def initialize(source) 21 | @source = source 22 | end 23 | def spec(name, version = nil, &block) 24 | @source[name] ||= [] 25 | unless version 26 | Spec.new(@source[name]).instance_eval(&block) if block 27 | else 28 | Spec.new(@source[name]).version(version, &block) 29 | end 30 | @source[name] = @source[name].sort_by{|a| Manifest::Version.new(a[:version])}.reverse 31 | end 32 | end 33 | 34 | class Spec 35 | def initialize(spec) 36 | @spec = spec 37 | end 38 | def version(name, &block) 39 | @spec << { :version => name, :dependencies => {} } 40 | Version.new(@spec.last[:dependencies]).instance_eval(&block) if block 41 | end 42 | end 43 | 44 | class Version 45 | def initialize(version) 46 | @version = version 47 | end 48 | def dependency(name, *requirement) 49 | @version[name] = requirement 50 | end 51 | end 52 | 53 | class << self 54 | def run!(sources, &block) 55 | Top.new(sources).instance_eval(&block) if block 56 | end 57 | end 58 | 59 | end 60 | 61 | def initialize 62 | clear! 63 | end 64 | def clear! 65 | self.sources = { } 66 | end 67 | def merge!(options = nil, &block) 68 | clear! if options && options[:clear] 69 | Dsl.run!(sources, &block) if block 70 | end 71 | def [](name) 72 | sources[name] ||= {} 73 | end 74 | 75 | private 76 | 77 | attr_accessor :sources 78 | 79 | end 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/librarian/cli/manifest_presenter.rb: -------------------------------------------------------------------------------- 1 | module Librarian 2 | class Cli 3 | class ManifestPresenter 4 | 5 | attr_accessor :cli, :manifests 6 | private :cli=, :manifests= 7 | 8 | def initialize(cli, manifests) 9 | self.cli = cli or raise ArgumentError, "cli required" 10 | self.manifests = manifests or raise ArgumentError, "manifests required" 11 | self.manifests_index = Hash[manifests.map{|m| [m.name, m]}] 12 | 13 | self.scope_level = 0 14 | end 15 | 16 | def present(names = [], options = { }) 17 | full = options[:detailed] 18 | full = !names.empty? if full.nil? 19 | 20 | names = manifests.map(&:name).sort if names.empty? 21 | assert_no_manifests_missing!(names) 22 | 23 | present_each(names, :detailed => full) 24 | end 25 | 26 | def present_one(manifest, options = { }) 27 | full = options[:detailed] 28 | 29 | say "#{manifest.name} (#{manifest.version})" do 30 | full or next 31 | 32 | present_one_source(manifest) 33 | present_one_dependencies(manifest) 34 | end 35 | end 36 | 37 | private 38 | 39 | def present_each(names, options) 40 | names.each do |name| 41 | manifest = manifest(name) 42 | present_one(manifest, options) 43 | end 44 | end 45 | 46 | def present_one_source(manifest) 47 | say "source: #{manifest.source}" 48 | end 49 | 50 | def present_one_dependencies(manifest) 51 | manifest.dependencies.empty? and return 52 | 53 | say "dependencies:" do 54 | deps = manifest.dependencies.sort_by(&:name) 55 | deps.each do |dependency| 56 | say "#{dependency.name} (#{dependency.requirement})" 57 | end 58 | end 59 | end 60 | 61 | attr_accessor :scope_level, :manifests_index 62 | 63 | def manifest(name) 64 | manifests_index[name] 65 | end 66 | 67 | def say(string) 68 | cli.say " " * scope_level << string 69 | scope { yield } if block_given? 70 | end 71 | 72 | def scope 73 | original_scope_level = scope_level 74 | self.scope_level = scope_level + 1 75 | yield 76 | ensure 77 | self.scope_level = original_scope_level 78 | end 79 | 80 | def assert_no_manifests_missing!(names) 81 | missing_names = names.reject{|name| manifest(name)} 82 | unless missing_names.empty? 83 | raise Error, "not found: #{missing_names.map(&:inspect).join(', ')}" 84 | end 85 | end 86 | 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/librarian/environment/runtime_cache.rb: -------------------------------------------------------------------------------- 1 | require "librarian/error" 2 | 3 | module Librarian 4 | class Environment 5 | class RuntimeCache 6 | 7 | class KeyspaceCache 8 | 9 | class << self 10 | private 11 | 12 | def delegate_to_backing_cache(*methods) 13 | methods.each do |method| 14 | define_method "#{method}" do |*args, &block| 15 | # TODO: When we drop ruby-1.8.7 support, use #public_send. 16 | runtime_cache.send(method, keyspace, *args, &block) 17 | end 18 | end 19 | end 20 | end 21 | 22 | attr_reader :runtime_cache, :keyspace 23 | 24 | def initialize(runtime_cache, keyspace) 25 | self.runtime_cache = runtime_cache 26 | self.keyspace = keyspace 27 | end 28 | 29 | delegate_to_backing_cache *[ 30 | :include?, 31 | :get, 32 | :put, 33 | :delete, 34 | :memo, 35 | :once, 36 | :[], 37 | :[]=, 38 | ] 39 | 40 | private 41 | 42 | attr_writer :runtime_cache, :keyspace 43 | 44 | end 45 | 46 | def initialize 47 | self.data = {} 48 | end 49 | 50 | def include?(keyspace, key) 51 | data.include?(combined_key(keyspace, key)) 52 | end 53 | 54 | def get(keyspace, key) 55 | data[combined_key(keyspace, key)] 56 | end 57 | 58 | def put(keyspace, key, value = nil) 59 | data[combined_key(keyspace, key)] = block_given? ? yield : value 60 | end 61 | 62 | def delete(keyspace, key) 63 | data.delete(combined_key(keyspace, key)) 64 | end 65 | 66 | def memo(keyspace, key) 67 | put(keyspace, key, yield) unless include?(keyspace, key) 68 | get(keyspace, key) 69 | end 70 | 71 | def once(keyspace, key) 72 | memo(keyspace, key) { yield ; nil } 73 | end 74 | 75 | def [](keyspace, key) 76 | get(keyspace, key) 77 | end 78 | 79 | def []=(keyspace, key, value) 80 | put(keyspace, key, value) 81 | end 82 | 83 | def keyspace(keyspace) 84 | KeyspaceCache.new(self, keyspace) 85 | end 86 | 87 | private 88 | 89 | attr_accessor :data 90 | 91 | def combined_key(keyspace, key) 92 | keyspace.kind_of?(String) or raise Error, "keyspace must be a string" 93 | keyspace.size > 0 or raise Error, "keyspace must not be empty" 94 | keyspace.size < 2**16 or raise Error, "keyspace must not be too large" 95 | key.kind_of?(String) or raise Error, "key must be a string" 96 | [keyspace.size.to_s(16).rjust(4, "0"), keyspace, key].join 97 | end 98 | 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/librarian/resolver.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/error' 2 | require 'librarian/resolver/implementation' 3 | require 'librarian/manifest_set' 4 | require 'librarian/resolution' 5 | 6 | module Librarian 7 | class Resolver 8 | 9 | attr_accessor :environment, :cyclic 10 | private :environment=, :cyclic= 11 | 12 | # Options: 13 | # cyclic: truthy if the resolver should permit cyclic resolutions 14 | def initialize(environment, options = { }) 15 | unrecognized_options = options.keys - [:cyclic] 16 | unrecognized_options.empty? or raise Error, 17 | "unrecognized options: #{unrecognized_options.join(", ")}" 18 | self.environment = environment 19 | self.cyclic = !!options[:cyclic] 20 | end 21 | 22 | def resolve(spec, partial_manifests = []) 23 | manifests = implementation(spec).resolve(partial_manifests) 24 | manifests or return 25 | enforce_consistency!(spec.dependencies, manifests) 26 | enforce_acyclicity!(manifests) unless cyclic 27 | manifests = sort(manifests) 28 | Resolution.new(spec.dependencies, manifests) 29 | end 30 | 31 | private 32 | 33 | def implementation(spec) 34 | Implementation.new(self, spec, :cyclic => cyclic) 35 | end 36 | 37 | def enforce_consistency!(dependencies, manifests) 38 | manifest_set = ManifestSet.new(manifests) 39 | return if manifest_set.in_compliance_with?(dependencies) 40 | return if manifest_set.consistent? 41 | 42 | debug { "Resolver Malfunctioned!" } 43 | errors = [] 44 | dependencies.sort_by(&:name).each do |d| 45 | m = manifests[d.name] 46 | if !m 47 | errors << ["Depends on #{d}", "Missing!"] 48 | elsif !d.satisfied_by?(m) 49 | errors << ["Depends on #{d}", "Found: #{m}"] 50 | end 51 | end 52 | unless errors.empty? 53 | errors.each do |a, b| 54 | debug { " #{a}" } 55 | debug { " #{b}" } 56 | end 57 | end 58 | manifests.values.sort_by(&:name).each do |manifest| 59 | errors = [] 60 | manifest.dependencies.sort_by(&:name).each do |d| 61 | m = manifests[d.name] 62 | if !m 63 | errors << ["Depends on: #{d}", "Missing!"] 64 | elsif !d.satisfied_by?(m) 65 | errors << ["Depends on: #{d}", "Found: #{m}"] 66 | end 67 | end 68 | unless errors.empty? 69 | debug { " #{manifest}" } 70 | errors.each do |a, b| 71 | debug { " #{a}" } 72 | debug { " #{b}" } 73 | end 74 | end 75 | end 76 | raise Error, "Resolver Malfunctioned!" 77 | end 78 | 79 | def enforce_acyclicity!(manifests) 80 | ManifestSet.cyclic?(manifests) or return 81 | debug { "Resolver Malfunctioned!" } 82 | raise Error, "Resolver Malfunctioned!" 83 | end 84 | 85 | def sort(manifests) 86 | ManifestSet.sort(manifests) 87 | end 88 | 89 | def debug(*args, &block) 90 | environment.logger.debug(*args, &block) 91 | end 92 | 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/librarian/dsl.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/dependency' 2 | require 'librarian/dsl/receiver' 3 | require 'librarian/dsl/target' 4 | 5 | module Librarian 6 | class Dsl 7 | 8 | class Error < Exception 9 | end 10 | 11 | attr_accessor :environment 12 | private :environment= 13 | 14 | class << self 15 | 16 | def run(environment, specfile = nil, precache_sources = [], &block) 17 | new(environment).run(specfile, precache_sources, &block) 18 | end 19 | 20 | private 21 | 22 | def dependency(name) 23 | dependency_name = name 24 | dependency_type = Dependency 25 | singleton_class = class << self; self end 26 | singleton_class.instance_eval do 27 | define_method(:dependency_name) { dependency_name } 28 | define_method(:dependency_type) { dependency_type } 29 | end 30 | end 31 | 32 | define_method(:source_types) { [] } 33 | 34 | def source(options) 35 | name = options.keys.first 36 | type = options[name] 37 | types = source_types 38 | types << [name, type] 39 | singleton_class = class << self; self end 40 | singleton_class.instance_eval do 41 | define_method(:source_types) { types } 42 | end 43 | end 44 | 45 | define_method(:source_shortcuts) { {} } 46 | 47 | def shortcut(name, options) 48 | instances = source_shortcuts 49 | instances[name] = options 50 | singleton_class = class << self; self end 51 | singleton_class.instance_eval do 52 | define_method(:source_shortcuts) { instances } 53 | end 54 | end 55 | 56 | def delegate_to_class(*names) 57 | names.each do |name| 58 | define_method(name) { self.class.send(name) } 59 | end 60 | end 61 | 62 | end 63 | 64 | delegate_to_class :dependency_name, :dependency_type, :source_types, :source_shortcuts 65 | 66 | def initialize(environment) 67 | self.environment = environment 68 | end 69 | 70 | def run(specfile = nil, sources = []) 71 | specfile, sources = nil, specfile if specfile.kind_of?(Array) && sources.empty? 72 | 73 | Target.new(self).tap do |target| 74 | target.precache_sources(sources) 75 | debug_named_source_cache("Pre-Cached Sources", target) 76 | 77 | specfile ||= Proc.new if block_given? 78 | receiver = Receiver.new(target) 79 | receiver.run(specfile) 80 | 81 | debug_named_source_cache("Post-Cached Sources", target) 82 | end.to_spec 83 | end 84 | 85 | def debug_named_source_cache(name, target) 86 | source_cache = target.source_cache 87 | debug { "#{name}:" } 88 | source_cache.each do |key, value| 89 | type = key[0] 90 | attributes = key[1...key.size] 91 | debug { " #{key.inspect}" } 92 | end 93 | end 94 | 95 | private 96 | 97 | def debug(*args, &block) 98 | environment.logger.debug(*args, &block) 99 | end 100 | 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/unit/action/clean_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/action/clean" 2 | 3 | module Librarian 4 | describe Action::Clean do 5 | 6 | let(:env) { double } 7 | let(:action) { described_class.new(env) } 8 | 9 | before do 10 | action.stub(:debug) 11 | end 12 | 13 | describe "#run" do 14 | 15 | describe "behavior" do 16 | 17 | after do 18 | action.run 19 | end 20 | 21 | describe "clearing the cache path" do 22 | 23 | before do 24 | action.stub(:clean_install_path) 25 | end 26 | 27 | context "when the cache path is missing" do 28 | before do 29 | env.stub_chain(:cache_path, :exist?) { false } 30 | end 31 | 32 | it "should not try to clear the cache path" do 33 | expect(env.cache_path).to receive(:rmtree).never 34 | end 35 | end 36 | 37 | context "when the cache path is present" do 38 | before do 39 | env.stub_chain(:cache_path, :exist?) { true } 40 | end 41 | 42 | it "should try to clear the cache path" do 43 | expect(env.cache_path).to receive(:rmtree).exactly(:once) 44 | end 45 | end 46 | 47 | end 48 | 49 | describe "clearing the install path" do 50 | 51 | before do 52 | action.stub(:clean_cache_path) 53 | end 54 | 55 | context "when the install path is missing" do 56 | before do 57 | env.stub_chain(:install_path, :exist?) { false } 58 | end 59 | 60 | it "should not try to clear the install path" do 61 | expect(env.install_path).to receive(:children).never 62 | end 63 | end 64 | 65 | context "when the install path is present" do 66 | before do 67 | env.stub_chain(:install_path, :exist?) { true } 68 | end 69 | 70 | it "should try to clear the install path" do 71 | children = [double, double, double] 72 | children.each do |child| 73 | child.stub(:file?) { false } 74 | end 75 | env.stub_chain(:install_path, :children) { children } 76 | 77 | children.each do |child| 78 | expect(child).to receive(:rmtree).exactly(:once) 79 | end 80 | end 81 | 82 | it "should only try to clear out directories from the install path, not files" do 83 | children = [double(:file? => false), double(:file? => true), double(:file? => true)] 84 | env.stub_chain(:install_path, :children) { children } 85 | 86 | children.select(&:file?).each do |child| 87 | expect(child).to receive(:rmtree).never 88 | end 89 | children.reject(&:file?).each do |child| 90 | expect(child).to receive(:rmtree).exactly(:once) 91 | end 92 | end 93 | end 94 | 95 | end 96 | 97 | end 98 | 99 | end 100 | 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/librarian/rspec/support/cli_macro.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "pathname" 3 | require "securerandom" 4 | require "stringio" 5 | require "thor" 6 | 7 | require "librarian/helpers" 8 | 9 | module Librarian 10 | module RSpec 11 | module Support 12 | module CliMacro 13 | 14 | class FakeShell < Thor::Shell::Basic 15 | def stdout 16 | @stdout ||= StringIO.new 17 | end 18 | def stderr 19 | @stderr ||= StringIO.new 20 | end 21 | def stdin 22 | raise "unsupported" 23 | end 24 | end 25 | 26 | class FileMatcher 27 | attr_accessor :rel_path, :content, :type, :base_path 28 | def initialize(rel_path, content, options = { }) 29 | self.rel_path = rel_path 30 | self.content = content 31 | self.type = options[:type] 32 | end 33 | def full_path 34 | @full_path ||= base_path + rel_path 35 | end 36 | def actual_content 37 | @actual_content ||= begin 38 | s = full_path.read 39 | s = JSON.parse(s) if type == :json 40 | s 41 | end 42 | end 43 | def matches?(base_path) 44 | base_path = Pathname(base_path) unless Pathname === base_path 45 | self.base_path = base_path 46 | 47 | full_path.file? && (!content || actual_content == content) 48 | end 49 | end 50 | 51 | def self.included(base) 52 | base.instance_exec do 53 | let(:project_path) do 54 | project_path = Pathname.new(__FILE__).expand_path 55 | project_path = project_path.dirname until project_path.join("Rakefile").exist? 56 | project_path 57 | end 58 | let(:tmp) { project_path.join("tmp/spec/cli") } 59 | let(:pwd) { tmp + SecureRandom.hex(8) } 60 | 61 | before { tmp.mkpath } 62 | before { pwd.mkpath } 63 | 64 | after { tmp.rmtree } 65 | end 66 | end 67 | 68 | def cli!(*args) 69 | @shell = FakeShell.new 70 | @exit_status = Dir.chdir(pwd) do 71 | described_class.with_environment do 72 | described_class.returning_status do 73 | described_class.start args, :shell => @shell 74 | end 75 | end 76 | end 77 | end 78 | 79 | def write_file!(path, content) 80 | path = pwd.join(path) 81 | path.dirname.mkpath 82 | path.open("wb"){|f| f.write(content)} 83 | end 84 | 85 | def write_json_file!(path, content) 86 | write_file! path, JSON.dump(content) 87 | end 88 | 89 | def strip_heredoc(text) 90 | Librarian::Helpers.strip_heredoc(text) 91 | end 92 | 93 | def shell 94 | @shell 95 | end 96 | 97 | def stdout 98 | shell.stdout.string 99 | end 100 | 101 | def stderr 102 | shell.stderr.string 103 | end 104 | 105 | def exit_status 106 | @exit_status 107 | end 108 | 109 | def have_file(rel_path, content = nil) 110 | FileMatcher.new(rel_path, content) 111 | end 112 | 113 | def have_json_file(rel_path, content) 114 | FileMatcher.new(rel_path, content, :type => :json) 115 | end 116 | 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/librarian/manifest.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | module Librarian 4 | class Manifest 5 | 6 | class Version 7 | include Comparable 8 | 9 | def initialize(*args) 10 | args = initialize_normalize_args(args) 11 | 12 | self.backing = Gem::Version.new(*args) 13 | end 14 | 15 | def to_gem_version 16 | backing 17 | end 18 | 19 | def <=>(other) 20 | to_gem_version <=> other.to_gem_version 21 | end 22 | 23 | def to_s 24 | to_gem_version.to_s 25 | end 26 | 27 | def inspect 28 | "#<#{self.class} #{to_s}>" 29 | end 30 | 31 | private 32 | 33 | def initialize_normalize_args(args) 34 | args.map do |arg| 35 | arg = [arg] if self.class === arg 36 | arg 37 | end 38 | end 39 | 40 | attr_accessor :backing 41 | end 42 | 43 | attr_accessor :source, :name, :extra 44 | private :source=, :name=, :extra= 45 | 46 | def initialize(source, name, extra = nil) 47 | assert_name_valid! name 48 | 49 | self.source = source 50 | self.name = name 51 | self.extra = extra 52 | end 53 | 54 | def to_s 55 | "#{name}/#{version} <#{source}>" 56 | end 57 | 58 | def version 59 | defined_version || fetched_version 60 | end 61 | 62 | def version=(version) 63 | self.defined_version = _normalize_version(version) 64 | end 65 | 66 | def version? 67 | return unless defined_version 68 | 69 | defined_version == fetched_version 70 | end 71 | 72 | def latest 73 | @latest ||= source.manifests(name).first 74 | end 75 | 76 | def outdated? 77 | latest.version > version 78 | end 79 | 80 | def dependencies 81 | defined_dependencies || fetched_dependencies 82 | end 83 | 84 | def dependencies=(dependencies) 85 | self.defined_dependencies = _normalize_dependencies(dependencies) 86 | end 87 | 88 | def dependencies? 89 | return unless defined_dependencies 90 | 91 | defined_dependencies.zip(fetched_dependencies).all? do |(a, b)| 92 | a.name == b.name && a.requirement == b.requirement 93 | end 94 | end 95 | 96 | def satisfies?(dependency) 97 | dependency.requirement.satisfied_by?(version) 98 | end 99 | 100 | def install! 101 | source.install!(self) 102 | end 103 | 104 | private 105 | 106 | attr_accessor :defined_version, :defined_dependencies 107 | 108 | def environment 109 | source.environment 110 | end 111 | 112 | def fetched_version 113 | @fetched_version ||= _normalize_version(fetch_version!) 114 | end 115 | 116 | def fetched_dependencies 117 | @fetched_dependencies ||= _normalize_dependencies(fetch_dependencies!) 118 | end 119 | 120 | def fetch_version! 121 | source.fetch_version(name, extra) 122 | end 123 | 124 | def fetch_dependencies! 125 | source.fetch_dependencies(name, version, extra) 126 | end 127 | 128 | def _normalize_version(version) 129 | Version.new(version) 130 | end 131 | 132 | def _normalize_dependencies(dependencies) 133 | if Hash === dependencies 134 | dependencies = dependencies.map{|k, v| Dependency.new(k, v, nil)} 135 | end 136 | dependencies.sort_by(&:name) 137 | end 138 | 139 | def assert_name_valid!(name) 140 | name =~ /\A\S(?:.*\S)?\z/ and return 141 | 142 | raise ArgumentError, "name (#{name.inspect}) must be sensible" 143 | end 144 | 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /spec/unit/action/install_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/error" 2 | require "librarian/action/install" 3 | 4 | module Librarian 5 | describe Action::Install do 6 | 7 | let(:env) { double(:specfile_name => "Specfile", :lockfile_name => "Specfile.lock") } 8 | let(:action) { described_class.new(env) } 9 | 10 | describe "#run" do 11 | 12 | describe "behavior" do 13 | 14 | describe "checking preconditions" do 15 | 16 | context "when the specfile is missing" do 17 | before do 18 | env.stub_chain(:specfile_path, :exist?) { false } 19 | end 20 | 21 | it "should raise an error explaining that the specfile is missing" do 22 | expect { action.run }.to raise_error(Error, "Specfile missing!") 23 | end 24 | end 25 | 26 | context "when the specfile is present but the lockfile is missing" do 27 | before do 28 | env.stub_chain(:specfile_path, :exist?) { true } 29 | env.stub_chain(:lockfile_path, :exist?) { false } 30 | end 31 | 32 | it "should raise an error explaining that the lockfile is missing" do 33 | expect { action.run }.to raise_error(Error, "Specfile.lock missing!") 34 | end 35 | end 36 | 37 | context "when the specfile and lockfile are present but inconsistent" do 38 | before do 39 | env.stub_chain(:specfile_path, :exist?) { true } 40 | env.stub_chain(:lockfile_path, :exist?) { true } 41 | action.stub(:spec_consistent_with_lock?) { false } 42 | end 43 | 44 | it "should raise an error explaining the inconsistenty" do 45 | expect { action.run }.to raise_error(Error, "Specfile and Specfile.lock are out of sync!") 46 | end 47 | end 48 | 49 | context "when the specfile and lockfile are present and consistent" do 50 | before do 51 | env.stub_chain(:specfile_path, :exist?) { true } 52 | env.stub_chain(:lockfile_path, :exist?) { true } 53 | action.stub(:spec_consistent_with_lock?) { true } 54 | action.stub(:perform_installation) 55 | end 56 | 57 | it "should not raise an error" do 58 | expect { action.run }.to_not raise_error 59 | end 60 | end 61 | 62 | end 63 | 64 | describe "performing the install" do 65 | 66 | def mock_manifest(i) 67 | double(:name => "manifest-#{i}") 68 | end 69 | 70 | let(:manifests) { 3.times.map{|i| mock_manifest(i)} } 71 | let(:sorted_manifests) { 4.times.map{|i| mock_manifest(i + 3)} } 72 | let(:install_path) { double } 73 | 74 | before do 75 | env.stub(:install_path) { install_path } 76 | action.stub(:check_preconditions) 77 | action.stub_chain(:lock, :manifests) { manifests } 78 | end 79 | 80 | after do 81 | action.run 82 | end 83 | 84 | it "should sort and install the manifests" do 85 | expect(ManifestSet).to receive(:sort).with(manifests).exactly(:once).ordered { sorted_manifests } 86 | 87 | install_path.stub(:exist?) { false } 88 | expect(install_path).to receive(:mkpath).exactly(:once).ordered 89 | 90 | sorted_manifests.each do |manifest| 91 | expect(manifest).to receive(:install!).exactly(:once).ordered 92 | end 93 | end 94 | 95 | it "should recreate the install path if it already exists" do 96 | action.stub(:sorted_manifests) { sorted_manifests } 97 | action.stub(:install_manifests) 98 | 99 | install_path.stub(:exist?) { true } 100 | expect(install_path).to receive(:rmtree) 101 | expect(install_path).to receive(:mkpath) 102 | end 103 | 104 | end 105 | 106 | end 107 | 108 | end 109 | 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/unit/algorithms_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/algorithms" 2 | 3 | module Librarian 4 | module Algorithms 5 | 6 | describe AdjacencyListDirectedGraph do 7 | 8 | describe :cyclic? do 9 | subject(:result) { described_class.cyclic?(graph) } 10 | 11 | context "with an empty graph" do 12 | let(:graph) { { } } 13 | it { should be false } 14 | end 15 | 16 | context "with a 1-node acyclic graph" do 17 | let(:graph) { { ?a => nil } } 18 | it { should be false } 19 | end 20 | 21 | context "with a 1-node cyclic graph" do 22 | let(:graph) { { ?a => [?a] } } 23 | it { should be true } 24 | end 25 | 26 | context "with a 2-node no-edge graph" do 27 | let(:graph) { { ?a => nil, ?b => nil } } 28 | it { should be false } 29 | end 30 | 31 | context "with a 2-node acyclic graph" do 32 | let(:graph) { { ?a => [?b], ?b => nil } } 33 | it { should be false } 34 | end 35 | 36 | context "with a 2-node cyclic graph" do 37 | let(:graph) { { ?a => [?b], ?b => [?a] } } 38 | it { should be true } 39 | end 40 | 41 | context "with a 2-scc graph" do 42 | let(:graph) { { ?a => [?b], ?b => [?a], ?c => [?d, ?b], ?d => [?c] } } 43 | it { should be true } 44 | end 45 | 46 | end 47 | 48 | describe :feedback_arc_set do 49 | subject(:result) { described_class.feedback_arc_set(graph) } 50 | 51 | context "with an empty graph" do 52 | let(:graph) { { } } 53 | it { should be_empty } 54 | end 55 | 56 | context "with a 1-node acyclic graph" do 57 | let(:graph) { { ?a => nil } } 58 | it { should be_empty } 59 | end 60 | 61 | context "with a 1-node cyclic graph" do 62 | let(:graph) { { ?a => [?a] } } 63 | it { should be == [[?a, ?a]] } 64 | end 65 | 66 | context "with a 2-node no-edge graph" do 67 | let(:graph) { { ?a => nil, ?b => nil } } 68 | it { should be_empty } 69 | end 70 | 71 | context "with a 2-node acyclic graph" do 72 | let(:graph) { { ?a => [?b], ?b => nil } } 73 | it { should be_empty } 74 | end 75 | 76 | context "with a 2-node cyclic graph" do 77 | let(:graph) { { ?a => [?b], ?b => [?a] } } 78 | it { should be == [[?a, ?b]] } # based on the explicit sort 79 | end 80 | 81 | context "with a 2-scc graph" do 82 | let(:graph) { { ?a => [?b], ?b => [?a], ?c => [?d, ?b], ?d => [?c] } } 83 | it { should be == [[?a, ?b], [?c, ?d]] } 84 | end 85 | 86 | end 87 | 88 | describe :tsort_cyclic do 89 | subject(:result) { described_class.tsort_cyclic(graph) } 90 | 91 | context "with an empty graph" do 92 | let(:graph) { { } } 93 | it { should be == [] } 94 | end 95 | 96 | context "with a 1-node acyclic graph" do 97 | let(:graph) { { ?a => nil } } 98 | it { should be == [?a] } 99 | end 100 | 101 | context "with a 1-node cyclic graph" do 102 | let(:graph) { { ?a => [?a] } } 103 | it { should be == [?a] } 104 | end 105 | 106 | context "with a 2-node no-edge graph" do 107 | let(:graph) { { ?a => nil, ?b => nil } } 108 | it { should be == [?a, ?b] } 109 | end 110 | 111 | context "with a 2-node acyclic graph" do 112 | let(:graph) { { ?a => [?b], ?b => nil } } 113 | it { should be == [?b, ?a] } # based on the explicit sort 114 | end 115 | 116 | context "with a 2-node cyclic graph" do 117 | let(:graph) { { ?a => [?b], ?b => [?a] } } 118 | it { should be == [?a, ?b] } # based on the explicit sort 119 | end 120 | 121 | context "with a 2-scc graph" do 122 | let(:graph) { { ?a => [?b], ?b => [?a], ?c => [?d, ?b], ?d => [?c] } } 123 | it { should be == [?a, ?b, ?c, ?d] } 124 | end 125 | 126 | end 127 | 128 | end 129 | 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/librarian/manifest_set.rb: -------------------------------------------------------------------------------- 1 | require "librarian/algorithms" 2 | 3 | module Librarian 4 | class ManifestSet 5 | 6 | class << self 7 | def shallow_strip(manifests, names) 8 | new(manifests).shallow_strip!(names).send(method_for(manifests)) 9 | end 10 | def deep_strip(manifests, names) 11 | new(manifests).deep_strip!(names).send(method_for(manifests)) 12 | end 13 | def shallow_keep(manifests, names) 14 | new(manifests).shallow_keep!(names).send(method_for(manifests)) 15 | end 16 | def deep_keep(manifests, names) 17 | new(manifests).deep_keep!(names).send(method_for(manifests)) 18 | end 19 | def cyclic?(manifests) 20 | manifests = Hash[manifests.map{|m| [m.name, m]}] if Array === manifests 21 | manifest_pairs = Hash[manifests.map{|k, m| [k, m.dependencies.map{|d| d.name}]}] 22 | adj_algs.cyclic?(manifest_pairs) 23 | end 24 | def sort(manifests) 25 | manifests = Hash[manifests.map{|m| [m.name, m]}] if Array === manifests 26 | manifest_pairs = Hash[manifests.map{|k, m| [k, m.dependencies.map{|d| d.name}]}] 27 | manifest_names = adj_algs.tsort_cyclic(manifest_pairs) 28 | manifest_names.map{|n| manifests[n]} 29 | end 30 | private 31 | def method_for(manifests) 32 | case manifests 33 | when Hash 34 | :to_hash 35 | when Array 36 | :to_a 37 | end 38 | end 39 | def adj_algs 40 | Algorithms::AdjacencyListDirectedGraph 41 | end 42 | end 43 | 44 | def initialize(manifests) 45 | self.index = Hash === manifests ? manifests.dup : index_by(manifests, &:name) 46 | end 47 | 48 | def to_a 49 | index.values 50 | end 51 | 52 | def to_hash 53 | index.dup 54 | end 55 | 56 | def dup 57 | self.class.new(index) 58 | end 59 | 60 | def shallow_strip(names) 61 | dup.shallow_strip!(names) 62 | end 63 | 64 | def shallow_strip!(names) 65 | assert_strings!(names) 66 | 67 | names.each do |name| 68 | index.delete(name) 69 | end 70 | self 71 | end 72 | 73 | def deep_strip(names) 74 | dup.deep_strip!(names) 75 | end 76 | 77 | def deep_strip!(names) 78 | strippables = dependencies_of(names) 79 | shallow_strip!(strippables) 80 | 81 | self 82 | end 83 | 84 | def shallow_keep(names) 85 | dup.shallow_keep!(names) 86 | end 87 | 88 | def shallow_keep!(names) 89 | assert_strings!(names) 90 | 91 | names = Set.new(names) unless Set === names 92 | index.reject! { |k, v| !names.include?(k) } 93 | self 94 | end 95 | 96 | def deep_keep(names) 97 | dup.conservative_strip!(names) 98 | end 99 | 100 | def deep_keep!(names) 101 | keepables = dependencies_of(names) 102 | shallow_keep!(keepables) 103 | 104 | self 105 | end 106 | 107 | def consistent? 108 | index.values.all? do |manifest| 109 | in_compliance_with?(manifest.dependencies) 110 | end 111 | end 112 | 113 | def in_compliance_with?(dependencies) 114 | dependencies.all? do |dependency| 115 | manifest = index[dependency.name] 116 | manifest && manifest.satisfies?(dependency) 117 | end 118 | end 119 | 120 | private 121 | 122 | attr_accessor :index 123 | 124 | def assert_strings!(names) 125 | non_strings = names.reject{|name| String === name} 126 | non_strings.empty? or raise TypeError, "names must all be strings" 127 | end 128 | 129 | # Straightforward breadth-first graph traversal algorithm. 130 | def dependencies_of(names) 131 | names = Array === names ? names.dup : names.to_a 132 | assert_strings!(names) 133 | 134 | deps = Set.new 135 | until names.empty? 136 | name = names.shift 137 | next if deps.include?(name) 138 | 139 | deps << name 140 | names.concat index[name].dependencies.map(&:name) 141 | end 142 | deps.to_a 143 | end 144 | 145 | def index_by(enum) 146 | Hash[enum.map{|obj| [yield(obj), obj]}] 147 | end 148 | 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/librarian/config/source.rb: -------------------------------------------------------------------------------- 1 | require "librarian/error" 2 | 3 | module Librarian 4 | module Config 5 | class Source 6 | 7 | RAW_KEY_SUFFIX_VALIDITY_PATTERN = 8 | /\A[A-Z0-9_]+\z/ 9 | CONFIG_KEY_VALIDITY_PATTERN = 10 | /\A[a-z][a-z0-9\-]+(?:\.[a-z0-9\-]+)*\z/ 11 | 12 | class << self 13 | def raw_key_suffix_validity_pattern 14 | RAW_KEY_SUFFIX_VALIDITY_PATTERN 15 | end 16 | def config_key_validity_pattern 17 | CONFIG_KEY_VALIDITY_PATTERN 18 | end 19 | end 20 | 21 | attr_accessor :adapter_name 22 | private :adapter_name= 23 | 24 | def initialize(adapter_name, options = { }) 25 | self.adapter_name = adapter_name 26 | 27 | self.forbidden_keys = options.delete(:forbidden_keys) || [] 28 | end 29 | 30 | def [](key) 31 | load! 32 | 33 | data[key] 34 | end 35 | 36 | def []=(key, value) 37 | key_permitted?(key) or raise Error, "key not permitted: #{key.inspect}" 38 | value_permitted?(key, value) or raise Error, "value for key #{key.inspect} not permitted: #{value.inspect}" 39 | 40 | load! 41 | if value.nil? 42 | data.delete(key) 43 | else 44 | data[key] = value 45 | end 46 | save(data) 47 | end 48 | 49 | def keys 50 | load! 51 | 52 | data.keys 53 | end 54 | 55 | private 56 | 57 | attr_accessor :data, :forbidden_keys 58 | 59 | def load! 60 | self.data = load unless data 61 | end 62 | 63 | def key_permitted?(key) 64 | String === key && 65 | config_key_validity_pattern === key && 66 | !forbidden_keys.any?{|k| k === key} 67 | end 68 | 69 | def value_permitted?(key, value) 70 | return true if value.nil? 71 | 72 | String === value 73 | end 74 | 75 | def raw_key_valid?(key) 76 | return false unless key.start_with?(raw_key_prefix) 77 | 78 | suffix = key[raw_key_prefix.size..-1] 79 | raw_key_suffix_validity_pattern =~ suffix 80 | end 81 | 82 | def raw_key_suffix_validity_pattern 83 | self.class.raw_key_suffix_validity_pattern 84 | end 85 | 86 | def config_key_valid?(key) 87 | config_key_validity_pattern === key 88 | end 89 | 90 | def config_key_validity_pattern 91 | self.class.config_key_validity_pattern 92 | end 93 | 94 | def raw_key_prefix 95 | @key_prefix ||= "LIBRARIAN_#{adapter_name.upcase}_" 96 | end 97 | 98 | def assert_raw_keys_valid!(raw) 99 | bad_keys = raw.keys.reject{|k| raw_key_valid?(k)} 100 | unless bad_keys.empty? 101 | config_path_s = config_path.to_s.inspect 102 | bad_keys_s = bad_keys.map(&:inspect).join(", ") 103 | raise Error, "config #{to_s} has bad keys: #{bad_keys_s}" 104 | end 105 | end 106 | 107 | def assert_config_keys_valid!(config) 108 | bad_keys = config.keys.reject{|k| config_key_valid?(k)} 109 | unless bad_keys.empty? 110 | bad_keys_s = bad_keys.map(&:inspect).join(", ") 111 | raise Error, "config has bad keys: #{bad_keys_s}" 112 | end 113 | end 114 | 115 | def assert_values_valid!(data) 116 | bad_data = data.reject{|k, v| String === v} 117 | bad_keys = bad_data.keys 118 | 119 | unless bad_keys.empty? 120 | bad_keys_s = bad_keys.map(&:inspect).join(", ") 121 | raise Error, "config has bad values for keys: #{bad_keys_s}" 122 | end 123 | end 124 | 125 | def translate_raw_to_config(raw) 126 | assert_raw_keys_valid!(raw) 127 | assert_values_valid!(raw) 128 | 129 | Hash[raw.map do |key, value| 130 | key = key[raw_key_prefix.size .. -1] 131 | key = key.downcase.gsub(/__/, ".").gsub(/_/, "-") 132 | [key, value] 133 | end] 134 | end 135 | 136 | def translate_config_to_raw(config) 137 | assert_config_keys_valid!(config) 138 | assert_values_valid!(config) 139 | 140 | Hash[config.map do |key, value| 141 | key = key.gsub(/\./, "__").gsub(/\-/, "_").upcase 142 | key = "#{raw_key_prefix}#{key}" 143 | [key, value] 144 | end] 145 | end 146 | 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/librarian/algorithms.rb: -------------------------------------------------------------------------------- 1 | require "set" 2 | require "tsort" 3 | 4 | module Librarian 5 | module Algorithms 6 | 7 | class GraphHash < Hash 8 | include TSort 9 | def tsort_each_node(&block) 10 | keys.sort.each(&block) # demand determinism 11 | end 12 | def tsort_each_child(node, &block) 13 | children = self[node] 14 | children && children.sort.each(&block) # demand determinism 15 | end 16 | class << self 17 | def from(hash) 18 | o = new 19 | hash.each{|k, v| o[k] = v} 20 | o 21 | end 22 | end 23 | end 24 | 25 | module AdjacencyListDirectedGraph 26 | extend self 27 | 28 | def cyclic?(graph) 29 | each_cyclic_strongly_connected_component_set(graph).any? 30 | end 31 | 32 | # Topological sort of the graph with an approximately minimal feedback arc 33 | # set removed. 34 | def tsort_cyclic(graph) 35 | fag = feedback_arc_graph(graph) 36 | reduced_graph = subtract_edges_graph(graph, fag) 37 | GraphHash.from(reduced_graph).tsort 38 | end 39 | 40 | # Returns an approximately minimal feedback arc set, lifted into a graph. 41 | def feedback_arc_graph(graph) 42 | edges_to_graph(feedback_arc_set(graph)) 43 | end 44 | 45 | # Returns an approximately minimal feedback arc set. 46 | def feedback_arc_set(graph) 47 | fas = feedback_arc_set_step0(graph) 48 | feedback_arc_set_step1(graph, fas) 49 | end 50 | 51 | private 52 | 53 | def edges_to_graph(edges) 54 | graph = {} 55 | edges.each do |(u, v)| 56 | graph[u] ||= [] 57 | graph[u] << v 58 | graph[v] ||= nil 59 | end 60 | graph 61 | end 62 | 63 | def subtract_edges_graph(graph, edges_graph) 64 | xgraph = {} 65 | graph.each do |n, vs| 66 | dests = edges_graph[n] 67 | xgraph[n] = !vs ? vs : !dests ? vs : vs - dests 68 | end 69 | xgraph 70 | end 71 | 72 | def each_cyclic_strongly_connected_component_set(graph) 73 | return enum_for(__method__, graph) unless block_given? 74 | GraphHash.from(graph).each_strongly_connected_component do |scc| 75 | if scc.size == 1 76 | n = scc.first 77 | vs = graph[n] or next 78 | vs.include?(n) or next 79 | end 80 | yield scc 81 | end 82 | end 83 | 84 | # Partitions the graph into its strongly connected component subgraphs, 85 | # removes the acyclic single-vertex components (multi-vertex components 86 | # are guaranteed to be cyclic), and yields each cyclic strongly connected 87 | # component. 88 | def each_cyclic_strongly_connected_component_graph(graph) 89 | return enum_for(__method__, graph) unless block_given? 90 | each_cyclic_strongly_connected_component_set(graph) do |scc| 91 | sccs = scc.to_set 92 | sccg = GraphHash.new 93 | scc.each do |n| 94 | vs = graph[n] 95 | sccg[n] = vs && vs.select{|v| sccs.include?(v)} 96 | end 97 | yield sccg 98 | end 99 | end 100 | 101 | # The 0th step of computing a feedback arc set: pick out vertices whose 102 | # removals will make the graph acyclic. 103 | def feedback_arc_set_step0(graph) 104 | fas = [] 105 | each_cyclic_strongly_connected_component_graph(graph) do |scc| 106 | scc.keys.sort.each do |n| # demand determinism 107 | vs = scc[n] or next 108 | vs.size > 0 or next 109 | vs.sort! # demand determinism 110 | fas << [n, vs.shift] 111 | break 112 | end 113 | end 114 | fas 115 | end 116 | 117 | # The 1st step of computing a feedback arc set: pick out vertices from the 118 | # 0th step whose removals turn out to be unnecessary. 119 | def feedback_arc_set_step1(graph, fas) 120 | reduced_graph = subtract_edges_graph(graph, edges_to_graph(fas)) 121 | fas.select do |(u, v)| 122 | reduced_graph[u] ||= [] 123 | reduced_graph[u] << v 124 | cyclic = cyclic?(reduced_graph) 125 | reduced_graph[u].pop if cyclic 126 | cyclic 127 | end 128 | end 129 | 130 | end 131 | 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/librarian/dependency.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | module Librarian 4 | class Dependency 5 | 6 | class Requirement 7 | def initialize(*args) 8 | args = initialize_normalize_args(args) 9 | 10 | self.backing = Gem::Requirement.create(*args) 11 | end 12 | 13 | def to_gem_requirement 14 | backing 15 | end 16 | 17 | def satisfied_by?(version) 18 | to_gem_requirement.satisfied_by?(version.to_gem_version) 19 | end 20 | 21 | def ==(other) 22 | to_gem_requirement == other.to_gem_requirement 23 | end 24 | 25 | def to_s 26 | to_gem_requirement.to_s 27 | end 28 | 29 | def inspect 30 | "#<#{self.class} #{to_s}>" 31 | end 32 | 33 | COMPATS_TABLE = { 34 | %w(= = ) => lambda{|s, o| s == o}, 35 | %w(= !=) => lambda{|s, o| s != o}, 36 | %w(= > ) => lambda{|s, o| s > o}, 37 | %w(= < ) => lambda{|s, o| s < o}, 38 | %w(= >=) => lambda{|s, o| s >= o}, 39 | %w(= <=) => lambda{|s, o| s <= o}, 40 | %w(= ~>) => lambda{|s, o| s >= o && s.release < o.bump}, 41 | %w(!= !=) => true, 42 | %w(!= > ) => true, 43 | %w(!= < ) => true, 44 | %w(!= >=) => true, 45 | %w(!= <=) => true, 46 | %w(!= ~>) => true, 47 | %w(> > ) => true, 48 | %w(> < ) => lambda{|s, o| s < o}, 49 | %w(> >=) => true, 50 | %w(> <=) => lambda{|s, o| s < o}, 51 | %w(> ~>) => lambda{|s, o| s < o.bump}, 52 | %w(< < ) => true, 53 | %w(< >=) => lambda{|s, o| s > o}, 54 | %w(< <=) => true, 55 | %w(< ~>) => lambda{|s, o| s > o}, 56 | %w(>= >=) => true, 57 | %w(>= <=) => lambda{|s, o| s <= o}, 58 | %w(>= ~>) => lambda{|s, o| s < o.bump}, 59 | %w(<= <=) => true, 60 | %w(<= ~>) => lambda{|s, o| s >= o}, 61 | %w(~> ~>) => lambda{|s, o| s < o.bump && s.bump > o}, 62 | } 63 | 64 | def consistent_with?(other) 65 | sgreq, ogreq = to_gem_requirement, other.to_gem_requirement 66 | sreqs, oreqs = sgreq.requirements, ogreq.requirements 67 | sreqs.all? do |sreq| 68 | oreqs.all? do |oreq| 69 | compatible?(sreq, oreq) 70 | end 71 | end 72 | end 73 | 74 | def inconsistent_with?(other) 75 | !consistent_with?(other) 76 | end 77 | 78 | protected 79 | 80 | attr_accessor :backing 81 | 82 | private 83 | 84 | def initialize_normalize_args(args) 85 | args.map do |arg| 86 | arg = arg.backing if self.class === arg 87 | arg 88 | end 89 | end 90 | 91 | def compatible?(a, b) 92 | a, b = b, a unless COMPATS_TABLE.include?([a.first, b.first]) 93 | r = COMPATS_TABLE[[a.first, b.first]] 94 | r = r.call(a.last, b.last) if r.respond_to?(:call) 95 | r 96 | end 97 | end 98 | 99 | attr_accessor :name, :requirement, :source 100 | private :name=, :requirement=, :source= 101 | 102 | def initialize(name, requirement, source) 103 | assert_name_valid! name 104 | 105 | self.name = name 106 | self.requirement = Requirement.new(requirement) 107 | self.source = source 108 | 109 | @manifests = nil 110 | end 111 | 112 | def manifests 113 | @manifests ||= cache_manifests! 114 | end 115 | 116 | def cache_manifests! 117 | source.manifests(name) 118 | end 119 | 120 | def satisfied_by?(manifest) 121 | manifest.satisfies?(self) 122 | end 123 | 124 | def to_s 125 | "#{name} (#{requirement}) <#{source}>" 126 | end 127 | 128 | def ==(other) 129 | !other.nil? && 130 | self.class == other.class && 131 | self.name == other.name && 132 | self.requirement == other.requirement && 133 | self.source == other.source 134 | end 135 | 136 | def consistent_with?(other) 137 | name != other.name || requirement.consistent_with?(other.requirement) 138 | end 139 | 140 | def inconsistent_with?(other) 141 | !consistent_with?(other) 142 | end 143 | 144 | private 145 | 146 | def assert_name_valid!(name) 147 | name =~ /\A\S(?:.*\S)?\z/ and return 148 | 149 | raise ArgumentError, "name (#{name.inspect}) must be sensible" 150 | end 151 | 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/librarian/lockfile/parser.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/manifest' 2 | require 'librarian/dependency' 3 | require 'librarian/manifest_set' 4 | 5 | module Librarian 6 | class Lockfile 7 | class Parser 8 | 9 | class ManifestPlaceholder 10 | attr_reader :source, :name, :version, :dependencies 11 | def initialize(source, name, version, dependencies) 12 | @source, @name, @version, @dependencies = source, name, version, dependencies 13 | end 14 | end 15 | 16 | attr_accessor :environment 17 | private :environment= 18 | 19 | def initialize(environment) 20 | self.environment = environment 21 | end 22 | 23 | def parse(string) 24 | lines = string.lines.map{|l| l.sub(/\s+\z/, '')}.reject(&:empty?) 25 | sources = extract_and_parse_sources(lines) 26 | manifests = compile(sources) 27 | manifests_index = Hash[manifests.map{|m| [m.name, m]}] 28 | raise StandardError, "Expected DEPENDENCIES topic!" unless lines.shift == "DEPENDENCIES" 29 | dependencies = extract_and_parse_dependencies(lines, manifests_index) 30 | Resolution.new(dependencies, manifests) 31 | end 32 | 33 | private 34 | 35 | def extract_and_parse_sources(lines) 36 | sources = [] 37 | while source_type_names.include?(lines.first) 38 | source = {} 39 | source_type_name = lines.shift 40 | source[:type] = source_type_names_map[source_type_name] 41 | options = {} 42 | while lines.first =~ /^ {2}([\w-]+):\s+(.+)$/ 43 | lines.shift 44 | options[$1.to_sym] = $2 45 | end 46 | source[:options] = options 47 | lines.shift # specs 48 | manifests = {} 49 | while lines.first =~ /^ {4}([\w-]+) \((.*)\)$/ 50 | lines.shift 51 | name = $1 52 | manifests[name] = {:version => $2, :dependencies => {}} 53 | while lines.first =~ /^ {6}([\w-]+) \((.*)\)$/ 54 | lines.shift 55 | manifests[name][:dependencies][$1] = $2.split(/,\s*/) 56 | end 57 | end 58 | source[:manifests] = manifests 59 | sources << source 60 | end 61 | sources 62 | end 63 | 64 | def extract_and_parse_dependencies(lines, manifests_index) 65 | dependencies = [] 66 | while lines.first =~ /^ {2}([\w-]+)(?: \((.*)\))?$/ 67 | lines.shift 68 | name, requirement = $1, $2.split(/,\s*/) 69 | dependencies << Dependency.new(name, requirement, manifests_index[name].source) 70 | end 71 | dependencies 72 | end 73 | 74 | def compile_placeholder_manifests(sources_ast) 75 | manifests = {} 76 | sources_ast.each do |source_ast| 77 | source_type = source_ast[:type] 78 | source = source_type.from_lock_options(environment, source_ast[:options]) 79 | source_ast[:manifests].each do |manifest_name, manifest_ast| 80 | manifests[manifest_name] = ManifestPlaceholder.new( 81 | source, 82 | manifest_name, 83 | manifest_ast[:version], 84 | manifest_ast[:dependencies].map{|k, v| Dependency.new(k, v, nil)} 85 | ) 86 | end 87 | end 88 | manifests 89 | end 90 | 91 | def compile(sources_ast) 92 | manifests = compile_placeholder_manifests(sources_ast) 93 | manifests = manifests.map do |name, manifest| 94 | dependencies = manifest.dependencies.map do |d| 95 | Dependency.new(d.name, d.requirement, manifests[d.name].source) 96 | end 97 | real = Manifest.new(manifest.source, manifest.name) 98 | real.version = manifest.version 99 | real.dependencies = manifest.dependencies 100 | real 101 | end 102 | ManifestSet.sort(manifests) 103 | end 104 | 105 | def dsl_class 106 | environment.dsl_class 107 | end 108 | 109 | def source_type_names_map 110 | @source_type_names_map ||= begin 111 | Hash[dsl_class.source_types.map{|t| [t[1].lock_name, t[1]]}] 112 | end 113 | end 114 | 115 | def source_type_names 116 | @source_type_names ||= begin 117 | dsl_class.source_types.map{|t| t[1].lock_name} 118 | end 119 | end 120 | 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/librarian/posix.rb: -------------------------------------------------------------------------------- 1 | require "open3" 2 | require "rbconfig" 3 | 4 | require "librarian/error" 5 | 6 | module Librarian 7 | module Posix 8 | 9 | module Platform 10 | extend self 11 | 12 | def win? 13 | host_os = RbConfig::CONFIG["host_os"].dup.freeze 14 | host_os =~ /mswin|mingw/ 15 | end 16 | end 17 | 18 | class << self 19 | 20 | # Cross-platform way of finding an executable in the $PATH. 21 | # 22 | # which('ruby') #=> /usr/bin/ruby 23 | # 24 | # From: 25 | # https://github.com/defunkt/hub/commit/353031307e704d860826fc756ff0070be5e1b430#L2R173 26 | def which(cmd) 27 | exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(';') : [''] 28 | ENV["PATH"].split(File::PATH_SEPARATOR).each do |path| 29 | path = File.expand_path(path) 30 | exts.each do |ext| 31 | exe = File.join(path, cmd + ext) 32 | return exe if File.file?(exe) && File.executable?(exe) 33 | end 34 | end 35 | nil 36 | end 37 | 38 | def which!(cmd) 39 | which(cmd) or raise Error, "cannot find #{cmd}" 40 | end 41 | 42 | end 43 | 44 | class CommandFailure < Error 45 | class << self 46 | def raise!(command, status, message) 47 | ex = new(message) 48 | ex.command = command 49 | ex.status = status 50 | ex.set_backtrace caller 51 | raise ex 52 | end 53 | end 54 | 55 | attr_accessor :command, :status 56 | end 57 | 58 | class << self 59 | 60 | def rescuing(*klasses) 61 | begin 62 | yield 63 | rescue *klasses 64 | end 65 | end 66 | 67 | # Hacky way to run a program because we need to update our own env and 68 | # working directory before running the command. Useful when we don't have 69 | # either fork or spawn or when we don't have fork and spawn doesn't 70 | # support all the options we need. 71 | def run_popen3!(command, options = { }) 72 | out, err = nil, nil 73 | chdir = options[:chdir].to_s if options[:chdir] 74 | env = options[:env] || { } 75 | old_env = Hash[env.keys.map{|k| [k, ENV[k]]}] 76 | out, err, wait = nil, nil, nil 77 | begin 78 | ENV.update env 79 | Dir.chdir(chdir || Dir.pwd) do 80 | IO.popen3(*command) do |i, o, e, w| 81 | rescuing(Errno::EBADF){ i.close } # jruby/1.9 can raise EBADF 82 | out, err, wait = o.read, e.read, w 83 | end 84 | end 85 | ensure 86 | ENV.update old_env 87 | end 88 | s = wait ? wait.value : $? # wait is 1.9+-only 89 | s.success? or CommandFailure.raise! command, s, err 90 | out 91 | end 92 | 93 | # Semi-hacky way to run a program because we're just reimplementing spawn. 94 | # Useful when we have fork but we don't have spawn or when we have fork 95 | # but spawn doesn't support all the options we need. 96 | def run_forkexec!(command, options = { }) 97 | i, o, e = IO.pipe, IO.pipe, IO.pipe 98 | pid = fork do 99 | $stdin.reopen i[0] 100 | $stdout.reopen o[1] 101 | $stderr.reopen e[1] 102 | [i[1], i[0], o[0], e[0]].each &:close 103 | ENV.update options[:env] || { } 104 | Dir.chdir options[:chdir].to_s if options[:chdir] 105 | exec *command 106 | end 107 | [i[0], i[1], o[1], e[1]].each &:close 108 | Process.waitpid pid 109 | $?.success? or CommandFailure.raise! command, $?, e[0].read 110 | o[0].read 111 | ensure 112 | [i, o, e].flatten(1).each{|io| io.close unless io.closed?} 113 | end 114 | 115 | # The sensible way to run a program. 116 | def run_spawn!(command, options = { }) 117 | i, o, e = IO.pipe, IO.pipe, IO.pipe 118 | opts = {:in => i[0], :out => o[1], :err => e[1]} 119 | opts[:chdir] = options[:chdir].to_s if options[:chdir] 120 | command = command.dup 121 | command.unshift options[:env] || { } 122 | command.push opts 123 | pid = Process.spawn(*command) 124 | [i[0], i[1], o[1], e[1]].each &:close 125 | Process.waitpid pid 126 | $?.success? or CommandFailure.raise! command, $?, e[0].read 127 | o[0].read 128 | ensure 129 | [i, o, e].flatten(1).each{|io| io.close unless io.closed?} 130 | end 131 | 132 | # jruby-1.7.9 can't fork and doesn't have a decent spawn 133 | # windows can't fork and doesn't have a decent spawn 134 | if defined?(JRuby) || Platform.win? 135 | 136 | alias_method :run!, :run_popen3! 137 | 138 | else 139 | 140 | if RUBY_VERSION < "1.9" 141 | 142 | alias_method :run!, :run_forkexec! 143 | 144 | else 145 | 146 | alias_method :run!, :run_spawn! 147 | 148 | end 149 | 150 | end 151 | 152 | end 153 | 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /spec/unit/spec_change_set_spec.rb: -------------------------------------------------------------------------------- 1 | require 'librarian' 2 | require 'librarian/spec_change_set' 3 | require 'librarian/mock' 4 | 5 | module Librarian 6 | describe SpecChangeSet do 7 | 8 | let(:env) { Mock::Environment.new } 9 | let(:resolver) { env.resolver } 10 | 11 | context "a simple root removal" do 12 | 13 | it "should work" do 14 | env.registry :clear => true do 15 | source 'source-1' do 16 | spec 'butter', '1.0' 17 | spec 'jam', '1.0' 18 | end 19 | end 20 | spec = env.dsl do 21 | src 'source-1' 22 | dep 'butter' 23 | dep 'jam' 24 | end 25 | lock = resolver.resolve(spec) 26 | expect(lock).to be_correct 27 | 28 | spec = env.dsl do 29 | src 'source-1' 30 | dep 'jam' 31 | end 32 | changes = described_class.new(env, spec, lock) 33 | expect(changes).to_not be_same 34 | 35 | manifests = ManifestSet.new(changes.analyze).to_hash 36 | expect(manifests).to have_key('jam') 37 | expect(manifests).to_not have_key('butter') 38 | end 39 | 40 | end 41 | 42 | context "a simple root add" do 43 | 44 | it "should work" do 45 | env.registry :clear => true do 46 | source 'source-1' do 47 | spec 'butter', '1.0' 48 | spec 'jam', '1.0' 49 | end 50 | end 51 | spec = env.dsl do 52 | src 'source-1' 53 | dep 'jam' 54 | end 55 | lock = resolver.resolve(spec) 56 | expect(lock).to be_correct 57 | 58 | spec = env.dsl do 59 | src 'source-1' 60 | dep 'butter' 61 | dep 'jam' 62 | end 63 | changes = described_class.new(env, spec, lock) 64 | expect(changes).to_not be_same 65 | manifests = ManifestSet.new(changes.analyze).to_hash 66 | expect(manifests).to have_key('jam') 67 | expect(manifests).to_not have_key('butter') 68 | end 69 | 70 | end 71 | 72 | context "a simple root change" do 73 | 74 | context "when the change is consistent" do 75 | 76 | it "should work" do 77 | env.registry :clear => true do 78 | source 'source-1' do 79 | spec 'butter', '1.0' 80 | spec 'jam', '1.0' 81 | spec 'jam', '1.1' 82 | end 83 | end 84 | spec = env.dsl do 85 | src 'source-1' 86 | dep 'butter' 87 | dep 'jam', '= 1.1' 88 | end 89 | lock = resolver.resolve(spec) 90 | expect(lock).to be_correct 91 | 92 | spec = env.dsl do 93 | src 'source-1' 94 | dep 'butter' 95 | dep 'jam', '>= 1.0' 96 | end 97 | changes = described_class.new(env, spec, lock) 98 | expect(changes).to_not be_same 99 | manifests = ManifestSet.new(changes.analyze).to_hash 100 | expect(manifests).to have_key('butter') 101 | expect(manifests).to have_key('jam') 102 | end 103 | 104 | end 105 | 106 | context "when the change is inconsistent" do 107 | 108 | it "should work" do 109 | env.registry :clear => true do 110 | source 'source-1' do 111 | spec 'butter', '1.0' 112 | spec 'jam', '1.0' 113 | spec 'jam', '1.1' 114 | end 115 | end 116 | spec = env.dsl do 117 | src 'source-1' 118 | dep 'butter' 119 | dep 'jam', '= 1.0' 120 | end 121 | lock = resolver.resolve(spec) 122 | expect(lock).to be_correct 123 | 124 | spec = env.dsl do 125 | src 'source-1' 126 | dep 'butter' 127 | dep 'jam', '>= 1.1' 128 | end 129 | changes = described_class.new(env, spec, lock) 130 | expect(changes).to_not be_same 131 | manifests = ManifestSet.new(changes.analyze).to_hash 132 | expect(manifests).to have_key('butter') 133 | expect(manifests).to_not have_key('jam') 134 | end 135 | 136 | end 137 | 138 | end 139 | 140 | context "a simple root source change" do 141 | it "should work" do 142 | env.registry :clear => true do 143 | source 'source-1' do 144 | spec 'butter', '1.0' 145 | end 146 | source 'source-2' do 147 | spec 'butter', '1.0' 148 | end 149 | end 150 | spec = env.dsl do 151 | src 'source-1' 152 | dep 'butter' 153 | end 154 | lock = resolver.resolve(spec) 155 | expect(lock).to be_correct 156 | 157 | spec = env.dsl do 158 | src 'source-1' 159 | dep 'butter', :src => 'source-2' 160 | end 161 | changes = described_class.new(env, spec, lock) 162 | expect(changes).to_not be_same 163 | manifests = ManifestSet.new(changes.analyze).to_hash 164 | expect(manifests).to_not have_key('butter') 165 | end 166 | end 167 | 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /spec/unit/lockfile/parser_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/helpers" 2 | require "librarian/lockfile/parser" 3 | require "librarian/mock" 4 | 5 | module Librarian 6 | describe Lockfile::Parser do 7 | 8 | let(:env) { Mock::Environment.new } 9 | let(:parser) { described_class.new(env) } 10 | let(:resolution) { parser.parse(lockfile) } 11 | 12 | context "a mock lockfile with one source and no dependencies" do 13 | let(:lockfile) do 14 | Helpers.strip_heredoc <<-LOCKFILE 15 | MOCK 16 | remote: source-a 17 | specs: 18 | 19 | DEPENDENCIES 20 | 21 | LOCKFILE 22 | end 23 | 24 | it "should give an empty list of dependencies" do 25 | expect(resolution.dependencies).to be_empty 26 | end 27 | 28 | it "should give an empty list of manifests" do 29 | expect(resolution.manifests).to be_empty 30 | end 31 | end 32 | 33 | context "a mock lockfile with one source and one dependency" do 34 | let(:lockfile) do 35 | Helpers.strip_heredoc <<-LOCKFILE 36 | MOCK 37 | remote: source-a 38 | specs: 39 | jelly (1.3.5) 40 | 41 | DEPENDENCIES 42 | jelly (!= 1.2.6, ~> 1.1) 43 | 44 | LOCKFILE 45 | end 46 | 47 | it "should give a list of one dependency" do 48 | expect(resolution).to have(1).dependencies 49 | end 50 | 51 | it "should give a dependency with the expected name" do 52 | dependency = resolution.dependencies.first 53 | 54 | expect(dependency.name).to eq "jelly" 55 | end 56 | 57 | it "should give a dependency with the expected requirement" do 58 | dependency = resolution.dependencies.first 59 | 60 | # Note: it must be this order because this order is lexicographically sorted. 61 | expect(dependency.requirement.to_s).to eq "!= 1.2.6, ~> 1.1" 62 | end 63 | 64 | it "should give a dependency wth the expected source" do 65 | dependency = resolution.dependencies.first 66 | source = dependency.source 67 | 68 | expect(source.name).to eq "source-a" 69 | end 70 | 71 | it "should give a list of one manifest" do 72 | expect(resolution).to have(1).manifests 73 | end 74 | 75 | it "should give a manifest with the expected name" do 76 | manifest = resolution.manifests.first 77 | 78 | expect(manifest.name).to eq "jelly" 79 | end 80 | 81 | it "should give a manifest with the expected version" do 82 | manifest = resolution.manifests.first 83 | 84 | expect(manifest.version.to_s).to eq "1.3.5" 85 | end 86 | 87 | it "should give a manifest with no dependencies" do 88 | manifest = resolution.manifests.first 89 | 90 | expect(manifest.dependencies).to be_empty 91 | end 92 | 93 | it "should give a manifest with the expected source" do 94 | manifest = resolution.manifests.first 95 | source = manifest.source 96 | 97 | expect(source.name).to eq "source-a" 98 | end 99 | 100 | it "should give the dependency and the manifest the same source instance" do 101 | dependency = resolution.dependencies.first 102 | manifest = resolution.manifests.first 103 | 104 | dependency_source = dependency.source 105 | manifest_source = manifest.source 106 | 107 | expect(manifest_source).to be dependency_source 108 | end 109 | end 110 | 111 | context "a mock lockfile with one source and a complex dependency" do 112 | let(:lockfile) do 113 | Helpers.strip_heredoc <<-LOCKFILE 114 | MOCK 115 | remote: source-a 116 | specs: 117 | butter (2.5.3) 118 | jelly (1.3.5) 119 | butter (< 3, >= 1.1) 120 | 121 | DEPENDENCIES 122 | jelly (!= 1.2.6, ~> 1.1) 123 | 124 | LOCKFILE 125 | end 126 | 127 | it "should give a list of one dependency" do 128 | expect(resolution).to have(1).dependencies 129 | end 130 | 131 | it "should have the expected dependency" do 132 | dependency = resolution.dependencies.first 133 | 134 | expect(dependency.name).to eq "jelly" 135 | end 136 | 137 | it "should give a list of all the manifests" do 138 | expect(resolution).to have(2).manifests 139 | end 140 | 141 | it "should include all the expected manifests" do 142 | manifests = ManifestSet.new(resolution.manifests) 143 | 144 | expect(manifests.to_hash.keys).to match_array( %w(butter jelly) ) 145 | end 146 | 147 | it "should have an internally consistent set of manifests" do 148 | manifests = ManifestSet.new(resolution.manifests) 149 | 150 | expect(manifests).to be_consistent 151 | end 152 | 153 | it "should have an externally consistent set of manifests" do 154 | dependencies = resolution.dependencies 155 | manifests = ManifestSet.new(resolution.manifests) 156 | 157 | expect(manifests).to be_in_compliance_with dependencies 158 | end 159 | end 160 | 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /lib/librarian/source/git.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'pathname' 3 | require 'digest' 4 | 5 | require 'librarian/error' 6 | require 'librarian/source/basic_api' 7 | require 'librarian/source/git/repository' 8 | require 'librarian/source/local' 9 | 10 | module Librarian 11 | module Source 12 | class Git 13 | include BasicApi 14 | include Local 15 | 16 | lock_name 'GIT' 17 | spec_options [:ref, :path] 18 | 19 | DEFAULTS = { 20 | :ref => 'master' 21 | } 22 | 23 | attr_accessor :environment 24 | private :environment= 25 | 26 | attr_accessor :uri, :ref, :sha, :path 27 | private :uri=, :ref=, :sha=, :path= 28 | 29 | def initialize(environment, uri, options) 30 | self.environment = environment 31 | self.uri = uri 32 | self.ref = options[:ref] || DEFAULTS[:ref] 33 | self.sha = options[:sha] 34 | self.path = options[:path] 35 | 36 | @repository = nil 37 | @repository_cache_path = nil 38 | 39 | ref.kind_of?(String) or raise TypeError, "ref must be a String" 40 | end 41 | 42 | def to_s 43 | path ? "#{uri}##{ref}(#{path})" : "#{uri}##{ref}" 44 | end 45 | 46 | def ==(other) 47 | other && 48 | self.class == other.class && 49 | self.uri == other.uri && 50 | self.ref == other.ref && 51 | self.path == other.path && 52 | (self.sha.nil? || other.sha.nil? || self.sha == other.sha) 53 | end 54 | 55 | def to_spec_args 56 | options = {} 57 | options.merge!(:ref => ref) if ref != DEFAULTS[:ref] 58 | options.merge!(:path => path) if path 59 | [uri, options] 60 | end 61 | 62 | def to_lock_options 63 | options = {:remote => uri, :ref => ref, :sha => sha} 64 | options.merge!(:path => path) if path 65 | options 66 | end 67 | 68 | def pinned? 69 | !!sha 70 | end 71 | 72 | def unpin! 73 | @sha = nil 74 | end 75 | 76 | def cache! 77 | repository_cached? and return or repository_cached! 78 | 79 | unless repository.git? 80 | repository.path.rmtree if repository.path.exist? 81 | repository.path.mkpath 82 | repository.clone!(uri) 83 | raise Error, "failed to clone #{uri}" unless repository.git? 84 | end 85 | 86 | # Probably unnecessary: nobody should be writing to our cache but us. 87 | # Just a precaution. 88 | repository_clean_once! 89 | 90 | unless sha 91 | repository_update_once! 92 | self.sha = fetch_sha_memo 93 | end 94 | 95 | unless repository.checked_out?(sha) 96 | repository_update_once! unless repository.has_commit?(sha) 97 | repository.checkout!(sha) 98 | # Probably unnecessary: if git fails to checkout, it should exit 99 | # nonzero, and we should expect Librarian::Posix::CommandFailure. 100 | raise Error, "failed to checkout #{sha}" unless repository.checked_out?(sha) 101 | end 102 | end 103 | 104 | # For tests 105 | def git_ops_count 106 | repository.git_ops_history.size 107 | end 108 | 109 | private 110 | 111 | attr_accessor :repository_cached 112 | alias repository_cached? repository_cached 113 | 114 | def repository_cached! 115 | self.repository_cached = true 116 | end 117 | 118 | def repository_cache_path 119 | @repository_cache_path ||= begin 120 | environment.cache_path + "source/git" + cache_key 121 | end 122 | end 123 | 124 | def repository 125 | @repository ||= begin 126 | Repository.new(environment, repository_cache_path) 127 | end 128 | end 129 | 130 | def filesystem_path 131 | @filesystem_path ||= path ? repository.path.join(path) : repository.path 132 | end 133 | 134 | def repository_clean_once! 135 | remote = repository.default_remote 136 | runtime_cache.once ['repository-clean', uri, ref].to_s do 137 | repository.reset_hard! 138 | repository.clean! 139 | end 140 | end 141 | 142 | def repository_update_once! 143 | remote = repository.default_remote 144 | runtime_cache.once ['repository-update', uri, remote, ref].to_s do 145 | repository.fetch! remote 146 | repository.fetch! remote, :tags => true 147 | end 148 | end 149 | 150 | def fetch_sha_memo 151 | remote = repository.default_remote 152 | runtime_cache.memo ['fetch-sha', uri, remote, ref].to_s do 153 | repository.hash_from(remote, ref) 154 | end 155 | end 156 | 157 | def cache_key 158 | @cache_key ||= begin 159 | uri_part = uri 160 | ref_part = "##{ref}" 161 | key_source = [uri_part, ref_part].join 162 | Digest::MD5.hexdigest(key_source)[0..15] 163 | end 164 | end 165 | 166 | def runtime_cache 167 | @runtime_cache ||= environment.runtime_cache.keyspace(self.class.name) 168 | end 169 | 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /spec/functional/source/git_spec.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require "pathname" 3 | require "securerandom" 4 | 5 | require "librarian/error" 6 | require "librarian/posix" 7 | require "librarian/source/git" 8 | require "librarian/source/git/repository" 9 | require "librarian/mock/environment" 10 | 11 | require "support/project_path_macro" 12 | 13 | describe Librarian::Source::Git do 14 | include Support::ProjectPathMacro 15 | 16 | let(:tmp_path) { project_path + "tmp/spec/functional/source/git" } 17 | after { tmp_path.rmtree if tmp_path && tmp_path.exist? } 18 | let(:env_project_path) { tmp_path + "project" } 19 | 20 | def cmd!(command) 21 | Librarian::Posix.run! command 22 | end 23 | 24 | def git!(command) 25 | cmd!([Librarian::Source::Git::Repository.bin] + command) 26 | end 27 | 28 | def new_env 29 | Librarian::Mock::Environment.new(:project_path => env_project_path) 30 | end 31 | 32 | context "when the remote is bad" do 33 | let(:remote) { tmp_path.join(SecureRandom.hex(8)).to_s } 34 | let(:env) { new_env } 35 | let(:source) { described_class.new(env, remote, {}) } 36 | 37 | it "fails when caching" do 38 | expect { source.cache! }.to raise_error Librarian::Error, 39 | /^fatal: repository .+ does not exist$/ # from git 40 | end 41 | end 42 | 43 | context "when the remote has a repo" do 44 | let(:remote) { tmp_path.join(SecureRandom.hex(8)).to_s } 45 | let(:git_source_path) { Pathname.new(remote) } 46 | let(:env) { new_env } 47 | let(:source) { described_class.new(env, remote, {}) } 48 | 49 | before do 50 | git_source_path.mkpath 51 | Dir.chdir(git_source_path) do 52 | git! %W[init] 53 | git! %W[config user.name Simba] 54 | git! %W[config user.email simba@savannah-pride.gov] 55 | FileUtils.touch "butter.txt" 56 | git! %W[add butter.txt] 57 | git! %W[commit -m #{"Initial Commit"}] 58 | end 59 | end 60 | 61 | let(:sha) do 62 | Dir.chdir(git_source_path) do 63 | git!(%W[rev-parse master]).strip 64 | end 65 | end 66 | 67 | context "when caching once" do 68 | it "has the expected sha" do 69 | expect{source.cache!}.to change{source.sha}.from(nil).to(sha) 70 | end 71 | 72 | it "records the history" do 73 | expect{source.cache!}.to change{source.git_ops_count}.from(0).to(9) 74 | end 75 | end 76 | 77 | context "when caching twice" do 78 | before { source.cache! } 79 | 80 | it "keeps the expected sha" do 81 | expect{source.cache!}.to_not change{source.sha} 82 | end 83 | 84 | it "runs git commands once" do 85 | expect{source.cache!}.to_not change{source.git_ops_count} 86 | end 87 | end 88 | 89 | context "when caching twice from different sources" do 90 | let(:other_source) { described_class.new(env, remote, {}) } 91 | before { other_source.cache! } 92 | 93 | it "has the expected sha" do 94 | expect{source.cache!}.to change{source.sha}.from(nil).to(sha) 95 | end 96 | 97 | it "records the history" do 98 | expect{source.cache!}.to change{source.git_ops_count}.from(0).to(1) 99 | end 100 | end 101 | 102 | context "when caching twice from different sources, second time with sha" do 103 | let(:other_source) { described_class.new(env, remote, {}) } 104 | before { other_source.cache! } 105 | 106 | let(:source) { described_class.new(env, remote, {:sha => sha}) } 107 | 108 | it "has the expected sha" do 109 | expect{source.cache!}.to_not change{source.sha} 110 | end 111 | 112 | it "records the history" do 113 | expect{source.cache!}.to change{source.git_ops_count}.from(0).to(1) 114 | end 115 | end 116 | 117 | context "when caching twice from different environments" do 118 | let(:other_source) { described_class.new(new_env, remote, {}) } 119 | before { other_source.cache! } 120 | 121 | it "has the expected sha" do 122 | expect{source.cache!}.to change{source.sha}.from(nil).to(sha) 123 | end 124 | 125 | it "records the history" do 126 | expect{source.cache!}.to change{source.git_ops_count}.from(0).to(8) 127 | end 128 | end 129 | 130 | context "when caching twice from different environments, second time with sha" do 131 | let(:other_source) { described_class.new(new_env, remote, {}) } 132 | before { other_source.cache! } 133 | 134 | let(:source) { described_class.new(env, remote, {:sha => sha}) } 135 | 136 | it "has the expected sha" do 137 | expect{source.cache!}.to_not change{source.sha} 138 | end 139 | 140 | it "records the history" do 141 | expect{source.cache!}.to change{source.git_ops_count}.from(0).to(3) 142 | end 143 | end 144 | 145 | context "when the sha is missing from a cached repo" do 146 | let(:other_source) { described_class.new(new_env, remote, {}) } 147 | before { other_source.cache! } 148 | 149 | before do 150 | Dir.chdir(git_source_path) do 151 | FileUtils.touch "jam.txt" 152 | git! %w[add jam.txt] 153 | git! %W[commit -m #{"Some Jam"}] 154 | end 155 | end 156 | 157 | let(:source) { described_class.new(env, remote, {:sha => sha}) } 158 | 159 | it "has a new remote sha" do 160 | expect(sha).to_not eq(other_source.sha) 161 | end 162 | 163 | it "has the expected sha" do 164 | expect{source.cache!}.to_not change{source.sha} 165 | end 166 | 167 | it "records the history" do 168 | expect{source.cache!}.to change{source.git_ops_count}.from(0).to(8) 169 | end 170 | end 171 | 172 | end 173 | 174 | end 175 | -------------------------------------------------------------------------------- /lib/librarian/dsl/target.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/spec' 2 | 3 | module Librarian 4 | class Dsl 5 | class Target 6 | 7 | class SourceShortcutDefinitionReceiver 8 | def initialize(target) 9 | singleton_class = class << self; self end 10 | singleton_class.class_eval do 11 | define_method(:source) do |options| 12 | target.source_from_options(options) 13 | end 14 | target.source_types.each do |source_type| 15 | name = source_type[0] 16 | define_method(name) do |*args| 17 | args.push({}) unless Hash === args.last 18 | target.source_from_params(name, *args) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | 25 | SCOPABLES = [:source, :sources] 26 | 27 | attr_accessor :dsl 28 | private :dsl= 29 | 30 | attr_reader :dependency_name, :dependency_type 31 | attr_reader :source_types, :source_types_map, :source_types_reverse_map, :source_type_names, :source_shortcuts 32 | attr_reader :dependencies, :source_cache, *SCOPABLES 33 | 34 | def initialize(dsl) 35 | self.dsl = dsl 36 | @dependency_name = dsl.dependency_name 37 | @dependency_type = dsl.dependency_type 38 | @source_types = dsl.source_types 39 | @source_types_map = Hash[source_types] 40 | @source_types_reverse_map = Hash[source_types.map{|pair| a, b = pair ; [b, a]}] 41 | @source_type_names = source_types.map{|t| t[0]} 42 | @source_cache = {} 43 | @source_shortcuts = {} 44 | @dependencies = [] 45 | SCOPABLES.each do |scopable| 46 | instance_variable_set(:"@#{scopable}", []) 47 | end 48 | dsl.source_shortcuts.each do |name, param| 49 | define_source_shortcut(name, param) 50 | end 51 | end 52 | 53 | def to_spec 54 | Spec.new(@sources, @dependencies) 55 | end 56 | 57 | def dependency(name, *args) 58 | options = args.last.is_a?(Hash) ? args.pop : {} 59 | source = source_from_options(options) || @source 60 | dep = dependency_type.new(name, args, source) 61 | @dependencies << dep 62 | end 63 | 64 | def source(name, param = nil, options = nil, &block) 65 | if !(Hash === name) && [Array, Hash, Proc].any?{|c| c === param} && !options && !block 66 | define_source_shortcut(name, param) 67 | elsif !(Hash === name) && !param && !options 68 | source = source_shortcuts[name] 69 | scope_or_directive(block) do 70 | @source = source 71 | @sources = @sources.dup << source 72 | end 73 | else 74 | name, param, options = *normalize_source_options(name, param, options || {}) 75 | source = source_from_params(name, param, options) 76 | scope_or_directive(block) do 77 | @source = source 78 | @sources = @sources.dup << source 79 | end 80 | end 81 | end 82 | 83 | def precache_sources(sources) 84 | sources.each do |source| 85 | key = [source_types_reverse_map[source.class], *source.to_spec_args] 86 | source_cache[key] = source 87 | end 88 | end 89 | 90 | def scope 91 | currents = { } 92 | SCOPABLES.each do |scopable| 93 | currents[scopable] = instance_variable_get(:"@#{scopable}").dup 94 | end 95 | yield 96 | ensure 97 | SCOPABLES.reverse.each do |scopable| 98 | instance_variable_set(:"@#{scopable}", currents[scopable]) 99 | end 100 | end 101 | 102 | def scope_or_directive(scoped_block = nil) 103 | unless scoped_block 104 | yield 105 | else 106 | scope do 107 | yield 108 | scoped_block.call 109 | end 110 | end 111 | end 112 | 113 | def normalize_source_options(name, param, options) 114 | if name.is_a?(Hash) 115 | extract_source_parts(name) 116 | else 117 | [name, param, options] 118 | end 119 | end 120 | 121 | def extract_source_parts(options) 122 | if name = source_type_names.find{|name| options.key?(name)} 123 | options = options.dup 124 | param = options.delete(name) 125 | [name, param, options] 126 | else 127 | nil 128 | end 129 | end 130 | 131 | def source_from_options(options) 132 | if options[:source] 133 | source_shortcuts[options[:source]] 134 | elsif source_parts = extract_source_parts(options) 135 | source_from_params(*source_parts) 136 | else 137 | nil 138 | end 139 | end 140 | 141 | def source_from_params(name, param, options) 142 | source_cache[[name, param, options]] ||= begin 143 | type = source_types_map[name] 144 | type.from_spec_args(environment, param, options) 145 | end 146 | end 147 | 148 | def source_from_source_shortcut_definition(definition) 149 | case definition 150 | when Array 151 | source_from_params(*definition) 152 | when Hash 153 | source_from_options(definition) 154 | when Proc 155 | receiver = SourceShortcutDefinitionReceiver.new(self) 156 | receiver.instance_eval(&definition) 157 | end 158 | end 159 | 160 | def define_source_shortcut(name, definition) 161 | source = source_from_source_shortcut_definition(definition) 162 | source_shortcuts[name] = source 163 | end 164 | 165 | def environment 166 | dsl.environment 167 | end 168 | 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /lib/librarian/source/git/repository.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | 3 | require "librarian/posix" 4 | 5 | module Librarian 6 | module Source 7 | class Git 8 | class Repository 9 | 10 | class << self 11 | def clone!(environment, path, repository_url) 12 | path = Pathname.new(path) 13 | path.mkpath 14 | git = new(environment, path) 15 | git.clone!(repository_url) 16 | git 17 | end 18 | 19 | def bin 20 | @bin ||= Posix.which!("git") 21 | end 22 | 23 | def git_version 24 | command = %W[#{bin} version --silent] 25 | Posix.run!(command).strip =~ /\Agit version (\d+(\.\d+)*)/ && $1 26 | end 27 | end 28 | 29 | attr_accessor :environment, :path, :git_ops_history 30 | private :environment=, :path=, :git_ops_history= 31 | 32 | def initialize(environment, path) 33 | self.environment = environment 34 | self.path = Pathname.new(path) 35 | self.git_ops_history = [] 36 | end 37 | 38 | def git? 39 | path.join('.git').exist? 40 | end 41 | 42 | def default_remote 43 | "origin" 44 | end 45 | 46 | def clone!(repository_url) 47 | command = %W(clone #{repository_url} . --quiet) 48 | run!(command, :chdir => true) 49 | end 50 | 51 | def checkout!(reference, options ={ }) 52 | command = %W(checkout #{reference} --quiet) 53 | command << "--force" if options[:force] 54 | run!(command, :chdir => true) 55 | end 56 | 57 | def fetch!(remote, options = { }) 58 | command = %W(fetch #{remote} --quiet) 59 | command << "--tags" if options[:tags] 60 | run!(command, :chdir => true) 61 | end 62 | 63 | def reset_hard! 64 | command = %W(reset --hard --quiet) 65 | run!(command, :chdir => true) 66 | end 67 | 68 | def clean! 69 | command = %w(clean -x -d --force --force) 70 | run!(command, :chdir => true) 71 | end 72 | 73 | def has_commit?(sha) 74 | command = %W(log -1 --no-color --format=tformat:%H #{sha}) 75 | run!(command, :chdir => true).strip == sha 76 | rescue Posix::CommandFailure => e 77 | false 78 | end 79 | 80 | def checked_out?(sha) 81 | current_commit_hash == sha 82 | end 83 | 84 | def remote_names 85 | command = %W(remote) 86 | run!(command, :chdir => true).strip.lines.map(&:strip) 87 | end 88 | 89 | def remote_branch_names 90 | remotes = remote_names.sort_by(&:length).reverse 91 | 92 | command = %W(branch -r --no-color) 93 | names = run!(command, :chdir => true).strip.lines.map(&:strip).to_a 94 | names.each{|n| n.gsub!(/\s*->.*$/, "")} 95 | names.reject!{|n| n =~ /\/HEAD$/} 96 | Hash[remotes.map do |r| 97 | matching_names = names.select{|n| n.start_with?("#{r}/")} 98 | matching_names.each{|n| names.delete(n)} 99 | matching_names.each{|n| n.slice!(0, r.size + 1)} 100 | [r, matching_names] 101 | end] 102 | end 103 | 104 | def hash_from(remote, reference) 105 | branch_names = remote_branch_names[remote] 106 | if branch_names.include?(reference) 107 | reference = "#{remote}/#{reference}" 108 | end 109 | 110 | command = %W(rev-list #{reference} -1) 111 | run!(command, :chdir => true).strip 112 | end 113 | 114 | def current_commit_hash 115 | command = %W(rev-parse HEAD --quiet) 116 | run!(command, :chdir => true).strip! 117 | end 118 | 119 | private 120 | 121 | def bin 122 | self.class.bin 123 | end 124 | 125 | def run!(args, options = { }) 126 | chdir = options.delete(:chdir) 127 | chdir = path.to_s if chdir == true 128 | 129 | silent = options.delete(:silent) 130 | pwd = chdir || Dir.pwd 131 | git_dir = File.join(path, ".git") if path 132 | env = {"GIT_DIR" => git_dir} 133 | 134 | command = [bin] 135 | command.concat(args) 136 | 137 | logging_command(command, :silent => silent, :pwd => pwd) do 138 | Posix.run!(command, :chdir => chdir, :env => env) 139 | end 140 | end 141 | 142 | def logging_command(command, options) 143 | silent = options.delete(:silent) 144 | 145 | pwd = Dir.pwd 146 | 147 | out = yield 148 | 149 | git_ops_history << command + [{:pwd => pwd}] 150 | 151 | unless silent 152 | if out.size > 0 153 | out.lines.each do |line| 154 | debug { " --> #{line}" } 155 | end 156 | else 157 | debug { " --- No output" } 158 | end 159 | end 160 | 161 | out 162 | 163 | rescue Posix::CommandFailure => e 164 | 165 | git_ops_history << command + [{:pwd => pwd}] 166 | 167 | status, stderr = e.status, e.message 168 | unless silent 169 | debug { " --- Exited with #{status}" } 170 | if stderr.size > 0 171 | stderr.lines.each do |line| 172 | debug { " --> #{line}" } 173 | end 174 | else 175 | debug { " --- No output" } 176 | end 177 | end 178 | 179 | raise e 180 | end 181 | 182 | def debug(*args, &block) 183 | environment.logger.debug(*args, &block) 184 | end 185 | 186 | def relative_path_to(path) 187 | environment.logger.relative_path_to(path) 188 | end 189 | 190 | end 191 | end 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /lib/librarian/environment.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | require 'net/http' 3 | require "uri" 4 | require "etc" 5 | 6 | require "librarian/helpers" 7 | require "librarian/support/abstract_method" 8 | 9 | require "librarian/error" 10 | require "librarian/config" 11 | require "librarian/lockfile" 12 | require "librarian/logger" 13 | require "librarian/specfile" 14 | require "librarian/resolver" 15 | require "librarian/dsl" 16 | require "librarian/source" 17 | require "librarian/version" 18 | require "librarian/environment/runtime_cache" 19 | 20 | module Librarian 21 | class Environment 22 | 23 | include Support::AbstractMethod 24 | 25 | attr_accessor :ui 26 | attr_reader :runtime_cache 27 | 28 | abstract_method :specfile_name, :dsl_class, :install_path 29 | 30 | def initialize(options = { }) 31 | @pwd = options.fetch(:pwd) { Dir.pwd } 32 | @env = options.fetch(:env) { ENV.to_hash } 33 | @home = options.fetch(:home) { default_home } 34 | @project_path = options[:project_path] 35 | @runtime_cache = RuntimeCache.new 36 | end 37 | 38 | def logger 39 | @logger ||= Logger.new(self) 40 | end 41 | 42 | def config_db 43 | @config_db ||= begin 44 | Config::Database.new(adapter_name, 45 | :pwd => @pwd, 46 | :env => @env, 47 | :home => @home, 48 | :project_path => @project_path, 49 | :specfile_name => default_specfile_name 50 | ) 51 | end 52 | end 53 | 54 | def default_specfile_name 55 | @default_specfile_name ||= begin 56 | capped = adapter_name.capitalize 57 | "#{capped}file" 58 | end 59 | end 60 | 61 | def project_path 62 | config_db.project_path 63 | end 64 | 65 | def specfile_name 66 | config_db.specfile_name 67 | end 68 | 69 | def specfile_path 70 | config_db.specfile_path 71 | end 72 | 73 | def specfile 74 | Specfile.new(self, specfile_path) 75 | end 76 | 77 | def adapter_module 78 | implementation? or return 79 | self.class.name.split("::")[0 ... -1].inject(Object, &:const_get) 80 | end 81 | 82 | def adapter_name 83 | implementation? or return 84 | Helpers.camel_cased_to_dasherized(self.class.name.split("::")[-2]) 85 | end 86 | 87 | def adapter_version 88 | implementation? or return 89 | adapter_module::VERSION 90 | end 91 | 92 | def lockfile_name 93 | config_db.lockfile_name 94 | end 95 | 96 | def lockfile_path 97 | config_db.lockfile_path 98 | end 99 | 100 | def lockfile 101 | Lockfile.new(self, lockfile_path) 102 | end 103 | 104 | def ephemeral_lockfile 105 | Lockfile.new(self, nil) 106 | end 107 | 108 | def resolver(options = { }) 109 | Resolver.new(self, resolver_options.merge(options)) 110 | end 111 | 112 | def resolver_options 113 | { 114 | :cyclic => resolver_permit_cyclic_reslutions?, 115 | } 116 | end 117 | 118 | def resolver_permit_cyclic_reslutions? 119 | false 120 | end 121 | 122 | def tmp_path 123 | part = config_db["tmp"] || "tmp" 124 | project_path.join(part) 125 | end 126 | 127 | def cache_path 128 | tmp_path.join("librarian/cache") 129 | end 130 | 131 | def scratch_path 132 | tmp_path.join("librarian/scratch") 133 | end 134 | 135 | def project_relative_path_to(path) 136 | Pathname.new(path).relative_path_from(project_path) 137 | end 138 | 139 | def spec 140 | specfile.read 141 | end 142 | 143 | def lock 144 | lockfile.read 145 | end 146 | 147 | def dsl(*args, &block) 148 | dsl_class.run(self, *args, &block) 149 | end 150 | 151 | def dsl_class 152 | adapter_module::Dsl 153 | end 154 | 155 | def version 156 | VERSION 157 | end 158 | 159 | def config_keys 160 | %[ 161 | ] 162 | end 163 | 164 | # The HTTP proxy specified in the environment variables: 165 | # * HTTP_PROXY 166 | # * HTTP_PROXY_USER 167 | # * HTTP_PROXY_PASS 168 | # Adapted from: 169 | # https://github.com/rubygems/rubygems/blob/v1.8.24/lib/rubygems/remote_fetcher.rb#L276-293 170 | def http_proxy_uri 171 | @http_proxy_uri ||= begin 172 | keys = %w( HTTP_PROXY HTTP_PROXY_USER HTTP_PROXY_PASS ) 173 | env = Hash[ENV. 174 | map{|k, v| [k.upcase, v]}. 175 | select{|k, v| keys.include?(k)}. 176 | reject{|k, v| v.nil? || v.empty?}] 177 | 178 | uri = env["HTTP_PROXY"] or return 179 | uri = "http://#{uri}" unless uri =~ /^(https?|ftp|file):/ 180 | uri = URI.parse(uri) 181 | uri.user ||= env["HTTP_PROXY_USER"] 182 | uri.password ||= env["HTTP_PROXY_PASS"] 183 | uri 184 | end 185 | end 186 | 187 | def net_http_class(host) 188 | no_proxy?(host) ? Net::HTTP : net_http_default_class 189 | end 190 | 191 | def inspect 192 | "#<#{self.class}:0x#{__id__.to_s(16)}>" 193 | end 194 | 195 | private 196 | 197 | def environment 198 | self 199 | end 200 | 201 | def implementation? 202 | self.class != ::Librarian::Environment 203 | end 204 | 205 | def default_home 206 | File.expand_path(ENV["HOME"] || Etc.getpwnam(Etc.getlogin).dir) 207 | end 208 | 209 | def no_proxy_list 210 | @no_proxy_list ||= begin 211 | list = ENV['NO_PROXY'] || ENV['no_proxy'] || "" 212 | list.split(/\s*,\s*/) + %w(localhost 127.0.0.1) 213 | end 214 | end 215 | 216 | def no_proxy?(host) 217 | no_proxy_list.any? do |host_addr| 218 | host.end_with?(host_addr) 219 | end 220 | end 221 | 222 | def net_http_default_class 223 | @net_http_default_class ||= begin 224 | p = http_proxy_uri 225 | p ? Net::HTTP::Proxy(p.host, p.port, p.user, p.password) : Net::HTTP 226 | end 227 | end 228 | 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /lib/librarian/config/database.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | 3 | require "librarian/config/file_source" 4 | require "librarian/config/hash_source" 5 | 6 | module Librarian 7 | module Config 8 | class Database 9 | 10 | class << self 11 | def library 12 | name.split("::").first.downcase 13 | end 14 | end 15 | 16 | attr_accessor :adapter_name 17 | private :adapter_name= 18 | 19 | attr_accessor :root, :assigned_specfile_name 20 | private :root=, :assigned_specfile_name= 21 | 22 | attr_accessor :underlying_env, :underlying_pwd, :underlying_home 23 | private :underlying_env=, :underlying_pwd=, :underlying_home= 24 | 25 | def initialize(adapter_name, options = { }) 26 | self.adapter_name = adapter_name or raise ArgumentError, "must provide adapter_name" 27 | 28 | options[:project_path] || options[:pwd] or raise ArgumentError, "must provide project_path or pwd" 29 | 30 | self.root = options[:project_path] && Pathname(options[:project_path]) 31 | self.assigned_specfile_name = options[:specfile_name] 32 | self.underlying_env = options[:env] or raise ArgumentError, "must provide env" 33 | self.underlying_pwd = options[:pwd] && Pathname(options[:pwd]) 34 | self.underlying_home = options[:home] && Pathname(options[:home]) 35 | end 36 | 37 | def global 38 | memo(__method__) { new_file_source(global_config_path) } 39 | end 40 | 41 | def env 42 | memo(__method__) { HashSource.new(adapter_name, :name => "environment", :raw => env_source_data) } 43 | end 44 | 45 | def local 46 | memo(__method__) { new_file_source(local_config_path) } 47 | end 48 | 49 | def [](key, scope = nil) 50 | case scope 51 | when "local", :local then local[key] 52 | when "env", :env then env[key] 53 | when "global", :global then global[key] 54 | when nil then local[key] || env[key] || global[key] 55 | else raise Error, "bad scope" 56 | end 57 | end 58 | 59 | def []=(key, scope, value) 60 | case scope 61 | when "local", :local then local[key] = value 62 | when "global", :global then global[key] = value 63 | else raise Error, "bad scope" 64 | end 65 | end 66 | 67 | def keys 68 | [local, env, global].inject([]){|a, e| a.concat(e.keys) ; a}.sort.uniq 69 | end 70 | 71 | def project_path 72 | root || specfile_path.dirname 73 | end 74 | 75 | def specfile_path 76 | if root 77 | root + (assigned_specfile_name || default_specfile_name) 78 | else 79 | env_specfile_path || default_specfile_path 80 | end 81 | end 82 | 83 | def specfile_name 84 | specfile_path.basename.to_s 85 | end 86 | 87 | def lockfile_path 88 | project_path + lockfile_name 89 | end 90 | 91 | def lockfile_name 92 | "#{specfile_name}.lock" 93 | end 94 | 95 | private 96 | 97 | def new_file_source(config_path) 98 | return unless config_path 99 | 100 | FileSource.new(adapter_name, 101 | :config_path => config_path, 102 | :forbidden_keys => [config_key, specfile_key] 103 | ) 104 | end 105 | 106 | def global_config_path 107 | env_global_config_path || default_global_config_path 108 | end 109 | 110 | def env_global_config_path 111 | memo(__method__) { env[config_key] } 112 | end 113 | 114 | def default_global_config_path 115 | underlying_home && underlying_home + config_name 116 | end 117 | 118 | def local_config_path 119 | root_local_config_path || env_local_config_path || default_local_config_path 120 | end 121 | 122 | def root_local_config_path 123 | root && root + config_name 124 | end 125 | 126 | def env_specfile_path 127 | memo(__method__) do 128 | path = env[specfile_key] 129 | path && Pathname(path) 130 | end 131 | end 132 | 133 | def default_specfile_path 134 | default_project_root_path + (assigned_specfile_name || default_specfile_name) 135 | end 136 | 137 | def env_local_config_path 138 | return unless env_specfile_path 139 | 140 | env_specfile_path.dirname + config_name 141 | end 142 | 143 | def default_local_config_path 144 | default_project_root_path + config_name 145 | end 146 | 147 | def default_project_root_path 148 | if root 149 | root 150 | else 151 | path = underlying_pwd 152 | path = path.dirname until project_root_path?(path) || path.dirname == path 153 | project_root_path?(path) ? path : underlying_pwd 154 | end 155 | end 156 | 157 | def project_root_path?(path) 158 | File.file?(path + default_specfile_name) 159 | end 160 | 161 | def config_key 162 | "config" 163 | end 164 | 165 | def specfile_key 166 | "#{adapter_name}file" 167 | end 168 | 169 | def default_specfile_name 170 | "#{adapter_name.capitalize}file" 171 | end 172 | 173 | def library 174 | self.class.library 175 | end 176 | 177 | def config_name_prefix 178 | ".#{library}" 179 | end 180 | 181 | def config_name 182 | File.join(*[config_name_prefix, adapter_name, "config"]) 183 | end 184 | 185 | def raw_key_prefix 186 | "#{library.upcase}_#{adapter_name.upcase}_" 187 | end 188 | 189 | def env_source_data 190 | prefix = raw_key_prefix 191 | 192 | data = underlying_env.dup 193 | data.reject!{|k, _| !k.start_with?(prefix) || k.size <= prefix.size} 194 | data 195 | end 196 | 197 | def memo(key) 198 | key = "@#{key}" 199 | instance_variable_set(key, yield) unless instance_variable_defined?(key) 200 | instance_variable_get(key) 201 | end 202 | 203 | end 204 | end 205 | end 206 | -------------------------------------------------------------------------------- /spec/unit/manifest_set_spec.rb: -------------------------------------------------------------------------------- 1 | require 'librarian' 2 | 3 | module Librarian 4 | describe ManifestSet do 5 | 6 | describe ".new" do 7 | let(:jelly) { double(:name => "jelly") } 8 | let(:butter) { double(:name => "butter") } 9 | let(:jam) { double(:name => "jam") } 10 | 11 | let(:array) { [jelly, butter, jam] } 12 | let(:hash) { {"jelly" => jelly, "butter" => butter, "jam" => jam} } 13 | 14 | context "with an array" do 15 | let(:set) { described_class.new(array) } 16 | 17 | it "should give back the array" do 18 | expect(set.to_a).to match_array( array ) 19 | end 20 | 21 | it "should give back the hash" do 22 | expect(set.to_hash).to eq hash 23 | end 24 | end 25 | 26 | context "with a hash" do 27 | let(:set) { described_class.new(hash) } 28 | 29 | it "should give back the array" do 30 | expect(set.to_a).to match_array( array ) 31 | end 32 | 33 | it "should give back the hash" do 34 | expect(set.to_hash).to eq hash 35 | end 36 | end 37 | end 38 | 39 | # Does not trace dependencies. 40 | # That's why it's "shallow". 41 | describe "#shallow_strip!" do 42 | let(:jelly) { double(:name => "jelly") } 43 | let(:butter) { double(:name => "butter") } 44 | let(:jam) { double(:name => "jam") } 45 | 46 | let(:set) { described_class.new([jelly, butter, jam]) } 47 | 48 | it "should not do anything when given no names" do 49 | set.shallow_strip!([]) 50 | 51 | expect(set.to_a).to match_array( [jelly, butter, jam] ) 52 | end 53 | 54 | it "should remove only the named elements" do 55 | set.shallow_strip!(["butter", "jam"]) 56 | 57 | expect(set.to_a).to match_array( [jelly] ) 58 | end 59 | 60 | it "should allow removing all the elements" do 61 | set.shallow_strip!(["jelly", "butter", "jam"]) 62 | 63 | expect(set.to_a).to match_array( [] ) 64 | end 65 | end 66 | 67 | # Does not trace dependencies. 68 | # That's why it's "shallow". 69 | describe "#shallow_keep!" do 70 | let(:jelly) { double(:name => "jelly") } 71 | let(:butter) { double(:name => "butter") } 72 | let(:jam) { double(:name => "jam") } 73 | 74 | let(:set) { described_class.new([jelly, butter, jam]) } 75 | 76 | it "should empty the set when given no names" do 77 | set.shallow_keep!([]) 78 | 79 | expect(set.to_a).to match_array( [] ) 80 | end 81 | 82 | it "should keep only the named elements" do 83 | set.shallow_keep!(["butter", "jam"]) 84 | 85 | expect(set.to_a).to match_array( [butter, jam] ) 86 | end 87 | 88 | it "should allow keeping all the elements" do 89 | set.shallow_keep!(["jelly", "butter", "jam"]) 90 | 91 | expect(set.to_a).to match_array( [jelly, butter, jam] ) 92 | end 93 | end 94 | 95 | describe "#deep_strip!" do 96 | def man(o) 97 | k, v = o.keys.first, o.values.first 98 | double(k, :name => k, :dependencies => deps(v)) 99 | end 100 | 101 | def deps(names) 102 | names.map{|n| double(:name => n)} 103 | end 104 | 105 | let(:a) { man("a" => %w[b c]) } 106 | let(:b) { man("b" => %w[c d]) } 107 | let(:c) { man("c" => %w[ ]) } 108 | let(:d) { man("d" => %w[ ]) } 109 | 110 | let(:e) { man("e" => %w[f g]) } 111 | let(:f) { man("f" => %w[g h]) } 112 | let(:g) { man("g" => %w[ ]) } 113 | let(:h) { man("h" => %w[ ]) } 114 | 115 | let(:set) { described_class.new([a, b, c, d, e, f, g, h]) } 116 | 117 | it "should not do anything when given no names" do 118 | set.deep_strip!([]) 119 | 120 | expect(set.to_a).to match_array( [a, b, c, d, e, f, g, h] ) 121 | end 122 | 123 | it "should remove just the named elements if they have no dependencies" do 124 | set.deep_strip!(["c", "h"]) 125 | 126 | expect(set.to_a).to match_array( [a, b, d, e, f, g] ) 127 | end 128 | 129 | it "should remove the named elements and all their dependencies" do 130 | set.deep_strip!(["b"]) 131 | 132 | expect(set.to_a).to match_array( [a, e, f, g, h] ) 133 | end 134 | 135 | it "should remove an entire tree of dependencies" do 136 | set.deep_strip!(["e"]) 137 | 138 | expect(set.to_a).to match_array( [a, b, c, d] ) 139 | end 140 | 141 | it "should allow removing all the elements" do 142 | set.deep_strip!(["a", "e"]) 143 | 144 | expect(set.to_a).to match_array( [] ) 145 | end 146 | end 147 | 148 | describe "#deep_keep!" do 149 | def man(o) 150 | k, v = o.keys.first, o.values.first 151 | double(k, :name => k, :dependencies => deps(v)) 152 | end 153 | 154 | def deps(names) 155 | names.map{|n| double(:name => n)} 156 | end 157 | 158 | let(:a) { man("a" => %w[b c]) } 159 | let(:b) { man("b" => %w[c d]) } 160 | let(:c) { man("c" => %w[ ]) } 161 | let(:d) { man("d" => %w[ ]) } 162 | 163 | let(:e) { man("e" => %w[f g]) } 164 | let(:f) { man("f" => %w[g h]) } 165 | let(:g) { man("g" => %w[ ]) } 166 | let(:h) { man("h" => %w[ ]) } 167 | 168 | let(:set) { described_class.new([a, b, c, d, e, f, g, h]) } 169 | 170 | it "should remove all the elements when given no names" do 171 | set.deep_keep!([]) 172 | 173 | expect(set.to_a).to match_array( [] ) 174 | end 175 | 176 | it "should keep just the named elements if they have no dependencies" do 177 | set.deep_keep!(["c", "h"]) 178 | 179 | expect(set.to_a).to match_array( [c, h] ) 180 | end 181 | 182 | it "should keep the named elements and all their dependencies" do 183 | set.deep_keep!(["b"]) 184 | 185 | expect(set.to_a).to match_array( [b, c, d] ) 186 | end 187 | 188 | it "should keep an entire tree of dependencies" do 189 | set.deep_keep!(["e"]) 190 | 191 | expect(set.to_a).to match_array( [e, f, g, h] ) 192 | end 193 | 194 | it "should allow keeping all the elements" do 195 | set.deep_keep!(["a", "e"]) 196 | 197 | expect(set.to_a).to match_array( [a, b, c, d, e, f, g, h] ) 198 | end 199 | end 200 | 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /spec/functional/source/git/repository_spec.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require "pathname" 3 | require "securerandom" 4 | 5 | require "librarian/posix" 6 | 7 | require "librarian/source/git/repository" 8 | 9 | require "librarian/mock/environment" 10 | 11 | require "support/project_path_macro" 12 | 13 | describe Librarian::Source::Git::Repository do 14 | include Support::ProjectPathMacro 15 | 16 | let(:env) { Librarian::Mock::Environment.new } 17 | 18 | let(:tmp_path) { project_path + "tmp/spec/functional/source/git/repository" } 19 | after { tmp_path.rmtree if tmp_path && tmp_path.exist? } 20 | let(:git_source_path) { tmp_path + SecureRandom.hex(16) } 21 | let(:branch) { "the-branch" } 22 | let(:tag) { "the-tag" } 23 | let(:atag) { "the-atag" } 24 | 25 | def cmd!(command) 26 | Librarian::Posix.run! command 27 | end 28 | 29 | def git!(command) 30 | cmd!([described_class.bin] + command) 31 | end 32 | 33 | before do 34 | git_source_path.mkpath 35 | Dir.chdir(git_source_path) do 36 | git! %W[init] 37 | git! %W[config user.name Simba] 38 | git! %W[config user.email simba@savannah-pride.gov] 39 | 40 | # master 41 | FileUtils.touch "butter.txt" 42 | git! %W[add butter.txt] 43 | git! %W[commit -m #{"Initial Commit"}] 44 | 45 | # branch 46 | git! %W[checkout -b #{branch} --quiet] 47 | FileUtils.touch "jam.txt" 48 | git! %W[add jam.txt] 49 | git! %W[commit -m #{"Branch Commit"}] 50 | git! %W[checkout master --quiet] 51 | 52 | # tag/atag 53 | git! %W[checkout -b deletable --quiet] 54 | FileUtils.touch "jelly.txt" 55 | git! %W[add jelly.txt] 56 | git! %W[commit -m #{"Tag Commit"}] 57 | git! %W[tag #{tag}] 58 | git! %W[tag -am #{"Annotated Tag Commit"} #{atag}] 59 | git! %W[checkout master --quiet] 60 | git! %W[branch -D deletable] 61 | end 62 | end 63 | 64 | describe ".bin" do 65 | specify { expect(described_class.bin).to_not be_empty } 66 | end 67 | 68 | describe ".git_version" do 69 | specify { expect(described_class.git_version).to match( /^\d+(\.\d+)+$/ ) } 70 | end 71 | 72 | context "the original" do 73 | subject { described_class.new(env, git_source_path) } 74 | 75 | it "should recognize it" do 76 | expect(subject).to be_git 77 | end 78 | 79 | it "should not list any remotes for it" do 80 | expect(subject.remote_names).to be_empty 81 | end 82 | 83 | it "should not list any remote branches for it" do 84 | expect(subject.remote_branch_names).to be_empty 85 | end 86 | 87 | it "should have divergent shas for master, branch, tag, and atag" do 88 | revs = %W[ master #{branch} #{tag} #{atag} ] 89 | rev_parse = proc{|rev| git!(%W[rev-parse #{rev} --quiet]).strip} 90 | shas = Dir.chdir(git_source_path){revs.map(&rev_parse)} 91 | expect(shas.map(&:class).uniq).to eq [String] 92 | expect(shas.map(&:size).uniq).to eq [40] 93 | expect(shas.uniq).to eq shas 94 | end 95 | end 96 | 97 | context "a clone" do 98 | let(:git_clone_path) { tmp_path + SecureRandom.hex(16) } 99 | subject { described_class.clone!(env, git_clone_path, git_source_path) } 100 | 101 | let(:master_sha) { subject.hash_from("origin", "master") } 102 | let(:branch_sha) { subject.hash_from("origin", branch) } 103 | let(:tag_sha) { subject.hash_from("origin", tag) } 104 | let(:atag_sha) { subject.hash_from("origin", atag) } 105 | 106 | it "should recognize it" do 107 | expect(subject).to be_git 108 | end 109 | 110 | it "should have a single remote for it" do 111 | expect(subject).to have(1).remote_names 112 | end 113 | 114 | it "should have a remote with the expected name" do 115 | expect(subject.remote_names.first).to eq "origin" 116 | end 117 | 118 | it "should have the remote branch" do 119 | expect(subject.remote_branch_names["origin"]).to include branch 120 | end 121 | 122 | it "should be checked out on the master" do 123 | expect(subject).to be_checked_out(master_sha) 124 | end 125 | 126 | context "checking for commits" do 127 | it "has the master commit" do 128 | expect(subject).to have_commit(master_sha) 129 | end 130 | 131 | it "has the branch commit" do 132 | expect(subject).to have_commit(branch_sha) 133 | end 134 | 135 | it "has the tag commit" do 136 | expect(subject).to have_commit(tag_sha) 137 | end 138 | 139 | it "has the atag commit" do 140 | expect(subject).to have_commit(atag_sha) 141 | end 142 | 143 | it "does not have a made-up commit" do 144 | expect(subject).to_not have_commit(SecureRandom.hex(20)) 145 | end 146 | 147 | it "does not have a tree commit" do 148 | master_tree_sha = Dir.chdir(git_source_path) do 149 | git!(%W[log -1 --no-color --format=tformat:%T master]).strip 150 | end 151 | expect(master_tree_sha).to match(/\A[0-9a-f]{40}\z/) # sanity 152 | expect(subject).to_not have_commit(master_tree_sha) 153 | end 154 | end 155 | 156 | context "checking out the branch" do 157 | before do 158 | subject.checkout! branch 159 | end 160 | 161 | it "should be checked out on the branch" do 162 | expect(subject).to be_checked_out(branch_sha) 163 | end 164 | 165 | it "should not be checked out on the master" do 166 | expect(subject).to_not be_checked_out(master_sha) 167 | end 168 | end 169 | 170 | context "checking out the tag" do 171 | before do 172 | subject.checkout! tag 173 | end 174 | 175 | it "should be checked out on the tag" do 176 | expect(subject).to be_checked_out(tag_sha) 177 | end 178 | 179 | it "should not be checked out on the master" do 180 | expect(subject).to_not be_checked_out(master_sha) 181 | end 182 | end 183 | 184 | context "checking out the annotated tag" do 185 | before do 186 | subject.checkout! atag 187 | end 188 | 189 | it "should be checked out on the annotated tag" do 190 | expect(subject).to be_checked_out(atag_sha) 191 | end 192 | 193 | it "should not be checked out on the master" do 194 | expect(subject).to_not be_checked_out(master_sha) 195 | end 196 | end 197 | end 198 | 199 | end 200 | -------------------------------------------------------------------------------- /spec/unit/dependency_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/dependency" 2 | 3 | describe Librarian::Dependency do 4 | 5 | describe "validations" do 6 | 7 | context "when the name is blank" do 8 | it "raises" do 9 | expect { described_class.new("", [], nil) }. 10 | to raise_error(ArgumentError, %{name ("") must be sensible}) 11 | end 12 | end 13 | 14 | context "when the name has leading whitespace" do 15 | it "raises" do 16 | expect { described_class.new(" the-name", [], nil) }. 17 | to raise_error(ArgumentError, %{name (" the-name") must be sensible}) 18 | end 19 | end 20 | 21 | context "when the name has trailing whitespace" do 22 | it "raises" do 23 | expect { described_class.new("the-name ", [], nil) }. 24 | to raise_error(ArgumentError, %{name ("the-name ") must be sensible}) 25 | end 26 | end 27 | 28 | context "when the name is a single character" do 29 | it "passes" do 30 | described_class.new("R", [], nil) 31 | end 32 | end 33 | 34 | end 35 | 36 | describe "#consistent_with?" do 37 | def req(s) described_class::Requirement.new(s) end 38 | def self.assert_consistent(a, b) 39 | /^(.+):(\d+):in `(.+)'$/ =~ caller.first 40 | line = $2.to_i 41 | 42 | title = "is consistent with #{a.inspect} and #{b.inspect}" 43 | module_eval <<-CODE, __FILE__, line 44 | it #{title.inspect} do 45 | a, b = req(#{a.inspect}), req(#{b.inspect}) 46 | expect(a).to be_consistent_with(b) 47 | expect(a).to_not be_inconsistent_with(b) 48 | expect(b).to be_consistent_with(a) 49 | expect(b).to_not be_inconsistent_with(a) 50 | end 51 | CODE 52 | end 53 | def self.refute_consistent(a, b) 54 | /^(.+):(\d+):in `(.+)'$/ =~ caller.first 55 | line = $2.to_i 56 | 57 | title = "is inconsistent with #{a.inspect} and #{b.inspect}" 58 | module_eval <<-CODE, __FILE__, line 59 | it #{title.inspect} do 60 | a, b = req(#{a.inspect}), req(#{b.inspect}) 61 | expect(a).to_not be_consistent_with(b) 62 | expect(a).to be_inconsistent_with(b) 63 | expect(b).to_not be_consistent_with(a) 64 | expect(b).to be_inconsistent_with(a) 65 | end 66 | CODE 67 | end 68 | 69 | # = = 70 | assert_consistent "3", "3" 71 | refute_consistent "3", "4" 72 | refute_consistent "3", "0" 73 | refute_consistent "0", "3" 74 | 75 | # = != 76 | assert_consistent "3", "!= 4" 77 | assert_consistent "3", "!= 0" 78 | refute_consistent "3", "!= 3" 79 | 80 | # = > 81 | assert_consistent "3", "> 2" 82 | refute_consistent "3", "> 3" 83 | refute_consistent "3", "> 4" 84 | 85 | # = < 86 | assert_consistent "3", "< 4" 87 | refute_consistent "3", "< 3" 88 | refute_consistent "3", "< 2" 89 | 90 | # = >= 91 | assert_consistent "3", ">= 2" 92 | assert_consistent "3", ">= 3" 93 | refute_consistent "3", ">= 4" 94 | 95 | # = <= 96 | assert_consistent "3", "<= 4" 97 | assert_consistent "3", "<= 3" 98 | refute_consistent "3", "<= 2" 99 | 100 | # = ~> 101 | assert_consistent "3.4.1", "~> 3.4.1" 102 | assert_consistent "3.4.2", "~> 3.4.1" 103 | refute_consistent "3.4", "~> 3.4.1" 104 | refute_consistent "3.5", "~> 3.4.1" 105 | 106 | # != != 107 | assert_consistent "!= 3", "!= 3" 108 | assert_consistent "!= 3", "!= 4" 109 | 110 | # != > 111 | assert_consistent "!= 3", "> 2" 112 | assert_consistent "!= 3", "> 3" 113 | assert_consistent "!= 3", "> 4" 114 | 115 | # != < 116 | assert_consistent "!= 3", "< 2" 117 | assert_consistent "!= 3", "< 3" 118 | assert_consistent "!= 3", "< 4" 119 | 120 | # != >= 121 | assert_consistent "!= 3", ">= 2" 122 | assert_consistent "!= 3", ">= 3" 123 | assert_consistent "!= 3", ">= 4" 124 | 125 | # != <= 126 | assert_consistent "!= 3", "<= 2" 127 | assert_consistent "!= 3", "<= 3" 128 | assert_consistent "!= 3", "<= 4" 129 | 130 | # != ~> 131 | assert_consistent "!= 3.4.1", "~> 3.4.1" 132 | assert_consistent "!= 3.4.2", "~> 3.4.1" 133 | assert_consistent "!= 3.5", "~> 3.4.1" 134 | 135 | # > > 136 | assert_consistent "> 3", "> 2" 137 | assert_consistent "> 3", "> 3" 138 | assert_consistent "> 3", "> 4" 139 | 140 | # > < 141 | assert_consistent "> 3", "< 4" 142 | refute_consistent "> 3", "< 3" 143 | refute_consistent "> 3", "< 2" 144 | 145 | # > >= 146 | assert_consistent "> 3", ">= 2" 147 | assert_consistent "> 3", ">= 3" 148 | assert_consistent "> 3", ">= 4" 149 | 150 | # > <= 151 | assert_consistent "> 3", "<= 4" 152 | refute_consistent "> 3", "<= 3" 153 | refute_consistent "> 3", "<= 2" 154 | 155 | # > ~> 156 | assert_consistent "> 3.3", "~> 3.4.1" 157 | assert_consistent "> 3.4.1", "~> 3.4.1" 158 | assert_consistent "> 3.4.2", "~> 3.4.1" 159 | refute_consistent "> 3.5", "~> 3.4.1" 160 | 161 | # < < 162 | assert_consistent "< 3", "< 2" 163 | assert_consistent "< 3", "< 3" 164 | assert_consistent "< 3", "< 4" 165 | 166 | # < >= 167 | assert_consistent "< 3", ">= 2" 168 | refute_consistent "< 3", ">= 3" 169 | refute_consistent "< 3", ">= 4" 170 | 171 | # < <= 172 | assert_consistent "< 3", "<= 2" 173 | assert_consistent "< 3", "<= 3" 174 | assert_consistent "< 3", "<= 4" 175 | 176 | # >= >= 177 | assert_consistent ">= 3", ">= 2" 178 | assert_consistent ">= 3", ">= 3" 179 | assert_consistent ">= 3", ">= 4" 180 | 181 | # >= <= 182 | assert_consistent ">= 3", "<= 4" 183 | assert_consistent ">= 3", "<= 3" 184 | refute_consistent ">= 3", "<= 2" 185 | 186 | # >= ~> 187 | assert_consistent ">= 3.3", "~> 3.4.1" 188 | assert_consistent ">= 3.4.1", "~> 3.4.1" 189 | assert_consistent ">= 3.4.2", "~> 3.4.1" 190 | refute_consistent ">= 3.5", "~> 3.4.1" 191 | 192 | # <= <= 193 | assert_consistent "<= 3", "<= 2" 194 | assert_consistent "<= 3", "<= 3" 195 | assert_consistent "<= 3", "<= 4" 196 | 197 | # <= ~> 198 | assert_consistent "<= 3.5", "~> 3.4.1" 199 | assert_consistent "<= 3.4.1", "~> 3.4.1" 200 | assert_consistent "<= 3.4.2", "~> 3.4.1" 201 | refute_consistent "<= 3.3", "~> 3.4.1" 202 | 203 | # ~> ~> 204 | assert_consistent "~> 3.4.1", "~> 3.4.1" 205 | assert_consistent "~> 3.4.2", "~> 3.4.1" 206 | assert_consistent "~> 3.3", "~> 3.4.1" 207 | refute_consistent "~> 3.3.3", "~> 3.4.1" 208 | refute_consistent "~> 3.5", "~> 3.4.1" 209 | refute_consistent "~> 3.5.4", "~> 3.4.1" 210 | end 211 | 212 | end 213 | -------------------------------------------------------------------------------- /spec/unit/dsl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'librarian' 2 | require 'librarian/mock' 3 | 4 | module Librarian 5 | module Mock 6 | 7 | describe Dsl do 8 | 9 | let(:env) { Environment.new } 10 | 11 | context "a single source and a single dependency with a blank name" do 12 | it "should not not run with a blank name" do 13 | expect do 14 | env.dsl do 15 | src 'source-1' 16 | dep '' 17 | end 18 | end.to raise_error(ArgumentError, %{name ("") must be sensible}) 19 | end 20 | end 21 | 22 | context "a simple specfile - a single source, a single dependency, no transitive dependencies" do 23 | 24 | it "should run with a hash source" do 25 | spec = env.dsl do 26 | dep 'dependency-1', 27 | :src => 'source-1' 28 | end 29 | expect(spec.dependencies).to_not be_empty 30 | expect(spec.dependencies.first.name).to eq 'dependency-1' 31 | expect(spec.dependencies.first.source.name).to eq 'source-1' 32 | expect(spec.sources).to be_empty 33 | end 34 | 35 | it "should run with a shortcut source" do 36 | spec = env.dsl do 37 | dep 'dependency-1', 38 | :source => :a 39 | end 40 | expect(spec.dependencies).to_not be_empty 41 | expect(spec.dependencies.first.name).to eq 'dependency-1' 42 | expect(spec.dependencies.first.source.name).to eq 'source-a' 43 | expect(spec.sources).to be_empty 44 | end 45 | 46 | it "should run with a block hash source" do 47 | spec = env.dsl do 48 | source :src => 'source-1' do 49 | dep 'dependency-1' 50 | end 51 | end 52 | expect(spec.dependencies).to_not be_empty 53 | expect(spec.dependencies.first.name).to eq 'dependency-1' 54 | expect(spec.dependencies.first.source.name).to eq 'source-1' 55 | expect(spec.sources).to be_empty 56 | end 57 | 58 | it "should run with a block named source" do 59 | spec = env.dsl do 60 | src 'source-1' do 61 | dep 'dependency-1' 62 | end 63 | end 64 | expect(spec.dependencies).to_not be_empty 65 | expect(spec.dependencies.first.name).to eq 'dependency-1' 66 | expect(spec.dependencies.first.source.name).to eq 'source-1' 67 | expect(spec.sources).to be_empty 68 | end 69 | 70 | it "should run with a default hash source" do 71 | spec = env.dsl do 72 | source :src => 'source-1' 73 | dep 'dependency-1' 74 | end 75 | expect(spec.dependencies).to_not be_empty 76 | expect(spec.dependencies.first.name).to eq 'dependency-1' 77 | expect(spec.dependencies.first.source.name).to eq 'source-1' 78 | expect(spec.sources).to_not be_empty 79 | expect(spec.dependencies.first.source).to eq spec.sources.first 80 | end 81 | 82 | it "should run with a default named source" do 83 | spec = env.dsl do 84 | src 'source-1' 85 | dep 'dependency-1' 86 | end 87 | expect(spec.dependencies).to_not be_empty 88 | expect(spec.dependencies.first.name).to eq 'dependency-1' 89 | expect(spec.dependencies.first.source.name).to eq 'source-1' 90 | expect(spec.sources).to_not be_empty 91 | expect(spec.dependencies.first.source).to eq spec.sources.first 92 | end 93 | 94 | it "should run with a default shortcut source" do 95 | spec = env.dsl do 96 | source :a 97 | dep 'dependency-1' 98 | end 99 | expect(spec.dependencies).to_not be_empty 100 | expect(spec.dependencies.first.name).to eq 'dependency-1' 101 | expect(spec.dependencies.first.source.name).to eq 'source-a' 102 | expect(spec.sources).to_not be_empty 103 | expect(spec.dependencies.first.source).to eq spec.sources.first 104 | end 105 | 106 | it "should run with a shortcut source hash definition" do 107 | spec = env.dsl do 108 | source :b, :src => 'source-b' 109 | dep 'dependency-1', :source => :b 110 | end 111 | expect(spec.dependencies).to_not be_empty 112 | expect(spec.dependencies.first.name).to eq 'dependency-1' 113 | expect(spec.dependencies.first.source.name).to eq 'source-b' 114 | expect(spec.sources).to be_empty 115 | end 116 | 117 | it "should run with a shortcut source block definition" do 118 | spec = env.dsl do 119 | source :b, proc { src 'source-b' } 120 | dep 'dependency-1', :source => :b 121 | end 122 | expect(spec.dependencies).to_not be_empty 123 | expect(spec.dependencies.first.name).to eq 'dependency-1' 124 | expect(spec.dependencies.first.source.name).to eq 'source-b' 125 | expect(spec.sources).to be_empty 126 | end 127 | 128 | it "should run with a default shortcut source hash definition" do 129 | spec = env.dsl do 130 | source :b, :src => 'source-b' 131 | source :b 132 | dep 'dependency-1' 133 | end 134 | expect(spec.dependencies).to_not be_empty 135 | expect(spec.dependencies.first.name).to eq 'dependency-1' 136 | expect(spec.dependencies.first.source.name).to eq 'source-b' 137 | expect(spec.sources).to_not be_empty 138 | expect(spec.sources.first.name).to eq 'source-b' 139 | end 140 | 141 | it "should run with a default shortcut source block definition" do 142 | spec = env.dsl do 143 | source :b, proc { src 'source-b' } 144 | source :b 145 | dep 'dependency-1' 146 | end 147 | expect(spec.dependencies).to_not be_empty 148 | expect(spec.dependencies.first.name).to eq 'dependency-1' 149 | expect(spec.dependencies.first.source.name).to eq 'source-b' 150 | expect(spec.sources).to_not be_empty 151 | expect(spec.sources.first.name).to eq 'source-b' 152 | end 153 | 154 | end 155 | 156 | context "validating source options" do 157 | 158 | it "should raise when given unrecognized optiosn options" do 159 | expect do 160 | env.dsl do 161 | dep 'dependency-1', 162 | :src => 'source-1', 163 | :huh => 'yikes' 164 | end 165 | end.to raise_error(Error, %{unrecognized options: huh}) 166 | end 167 | 168 | end 169 | 170 | end 171 | 172 | end 173 | end -------------------------------------------------------------------------------- /lib/librarian/spec_change_set.rb: -------------------------------------------------------------------------------- 1 | require 'librarian/helpers' 2 | 3 | require 'librarian/manifest_set' 4 | require 'librarian/resolution' 5 | require 'librarian/spec' 6 | 7 | module Librarian 8 | class SpecChangeSet 9 | 10 | attr_accessor :environment 11 | private :environment= 12 | attr_reader :spec, :lock 13 | 14 | def initialize(environment, spec, lock) 15 | self.environment = environment 16 | raise TypeError, "can't convert #{spec.class} into #{Spec}" unless Spec === spec 17 | raise TypeError, "can't convert #{lock.class} into #{Resolution}" unless Resolution === lock 18 | @spec, @lock = spec, lock 19 | end 20 | 21 | def same? 22 | @same ||= spec.dependencies.sort_by{|d| d.name} == lock.dependencies.sort_by{|d| d.name} 23 | end 24 | 25 | def changed? 26 | !same? 27 | end 28 | 29 | def spec_dependencies 30 | @spec_dependencies ||= spec.dependencies 31 | end 32 | def spec_dependency_names 33 | @spec_dependency_names ||= Set.new(spec_dependencies.map{|d| d.name}) 34 | end 35 | def spec_dependency_index 36 | @spec_dependency_index ||= Hash[spec_dependencies.map{|d| [d.name, d]}] 37 | end 38 | 39 | def lock_dependencies 40 | @lock_dependencies ||= lock.dependencies 41 | end 42 | def lock_dependency_names 43 | @lock_dependency_names ||= Set.new(lock_dependencies.map{|d| d.name}) 44 | end 45 | def lock_dependency_index 46 | @lock_dependency_index ||= Hash[lock_dependencies.map{|d| [d.name, d]}] 47 | end 48 | 49 | def lock_manifests 50 | @lock_manifests ||= lock.manifests 51 | end 52 | def lock_manifests_index 53 | @lock_manifests_index ||= ManifestSet.new(lock_manifests).to_hash 54 | end 55 | 56 | def removed_dependency_names 57 | @removed_dependency_names ||= lock_dependency_names - spec_dependency_names 58 | end 59 | 60 | # A dependency which is deleted from the specfile will, in the general case, 61 | # be removed conservatively. This means it might not actually be removed. 62 | # But if the dependency originally declared a source which is now non- 63 | # default, it must be removed, even if another dependency has a transitive 64 | # dependency on the one that was removed (which is the scenario in which 65 | # a conservative removal would not remove it). In this case, we must also 66 | # remove it explicitly so that it can be re-resolved from the default 67 | # source. 68 | def explicit_removed_dependency_names 69 | @explicit_removed_dependency_names ||= removed_dependency_names.reject do |name| 70 | lock_manifest = lock_manifests_index[name] 71 | spec.sources.include?(lock_manifest.source) 72 | end.to_set 73 | end 74 | 75 | def added_dependency_names 76 | @added_dependency_names ||= spec_dependency_names - lock_dependency_names 77 | end 78 | 79 | def nonmatching_added_dependency_names 80 | @nonmatching_added_dependency_names ||= added_dependency_names.reject do |name| 81 | spec_dependency = spec_dependency_index[name] 82 | lock_manifest = lock_manifests_index[name] 83 | if lock_manifest 84 | matching = true 85 | matching &&= spec_dependency.satisfied_by?(lock_manifest) 86 | matching &&= spec_dependency.source == lock_manifest.source 87 | matching 88 | else 89 | false 90 | end 91 | end.to_set 92 | end 93 | 94 | def common_dependency_names 95 | @common_dependency_names ||= lock_dependency_names & spec_dependency_names 96 | end 97 | 98 | def changed_dependency_names 99 | @changed_dependency_names ||= common_dependency_names.reject do |name| 100 | spec_dependency = spec_dependency_index[name] 101 | lock_dependency = lock_dependency_index[name] 102 | lock_manifest = lock_manifests_index[name] 103 | same = true 104 | same &&= spec_dependency.satisfied_by?(lock_manifest) 105 | same &&= spec_dependency.source == lock_dependency.source 106 | same 107 | end.to_set 108 | end 109 | 110 | def deep_keep_manifest_names 111 | @deep_keep_manifest_names ||= begin 112 | lock_dependency_names - ( 113 | removed_dependency_names + 114 | changed_dependency_names + 115 | nonmatching_added_dependency_names 116 | ) 117 | end 118 | end 119 | 120 | def shallow_strip_manifest_names 121 | @shallow_strip_manifest_names ||= begin 122 | explicit_removed_dependency_names + changed_dependency_names 123 | end 124 | end 125 | 126 | def inspect 127 | Helpers.strip_heredoc(<<-INSPECT) 128 | <##{self.class.name}: 129 | Removed: #{removed_dependency_names.to_a.join(", ")} 130 | ExplicitRemoved: #{explicit_removed_dependency_names.to_a.join(", ")} 131 | Added: #{added_dependency_names.to_a.join(", ")} 132 | NonMatchingAdded: #{nonmatching_added_dependency_names.to_a.join(", ")} 133 | Changed: #{changed_dependency_names.to_a.join(", ")} 134 | DeepKeep: #{deep_keep_manifest_names.to_a.join(", ")} 135 | ShallowStrip: #{shallow_strip_manifest_names.to_a.join(", ")} 136 | > 137 | INSPECT 138 | end 139 | 140 | # Returns an array of those manifests from the previous spec which should be kept, 141 | # based on inspecting the new spec against the locked resolution from the previous spec. 142 | def analyze 143 | @analyze ||= begin 144 | debug { "Analyzing spec and lock:" } 145 | 146 | if same? 147 | debug { " Same!" } 148 | return lock.manifests 149 | end 150 | 151 | debug { " Removed:" } ; removed_dependency_names.each { |name| debug { " #{name}" } } 152 | debug { " ExplicitRemoved:" } ; explicit_removed_dependency_names.each { |name| debug { " #{name}" } } 153 | debug { " Added:" } ; added_dependency_names.each { |name| debug { " #{name}" } } 154 | debug { " NonMatchingAdded:" } ; nonmatching_added_dependency_names.each { |name| debug { " #{name}" } } 155 | debug { " Changed:" } ; changed_dependency_names.each { |name| debug { " #{name}" } } 156 | debug { " DeepKeep:" } ; deep_keep_manifest_names.each { |name| debug { " #{name}" } } 157 | debug { " ShallowStrip:" } ; shallow_strip_manifest_names.each { |name| debug { " #{name}" } } 158 | 159 | manifests = ManifestSet.new(lock_manifests) 160 | manifests.deep_keep!(deep_keep_manifest_names) 161 | manifests.shallow_strip!(shallow_strip_manifest_names) 162 | manifests.to_a 163 | end 164 | end 165 | 166 | private 167 | 168 | def debug(*args, &block) 169 | environment.logger.debug(*args, &block) 170 | end 171 | 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /spec/unit/environment_spec.rb: -------------------------------------------------------------------------------- 1 | require "librarian/environment" 2 | 3 | require "support/with_env_macro" 4 | 5 | module Librarian 6 | describe Environment do 7 | include ::Support::WithEnvMacro 8 | 9 | let(:env) { described_class.new } 10 | 11 | describe "#adapter_module" do 12 | specify { expect(env.adapter_module).to be nil } 13 | end 14 | 15 | describe "#adapter_name" do 16 | specify { expect(env.adapter_name).to be nil } 17 | end 18 | 19 | describe "#adapter_version" do 20 | specify { expect(env.adapter_version).to be nil } 21 | end 22 | 23 | describe "computing the home" do 24 | 25 | context "with the HOME env var" do 26 | with_env "HOME" => "/path/to/home" 27 | 28 | it "finds the home" do 29 | env.stub(:adapter_name).and_return("cat") 30 | expect(env.config_db.underlying_home.to_s).to eq "/path/to/home" 31 | end 32 | end 33 | 34 | context "without the HOME env var" do 35 | let!(:real_home) { File.expand_path("~") } 36 | with_env "HOME" => nil 37 | 38 | it "finds the home" do 39 | env.stub(:adapter_name).and_return("cat") 40 | expect(env.config_db.underlying_home.to_s).to eq real_home 41 | end 42 | end 43 | 44 | end 45 | 46 | describe "#http_proxy_uri" do 47 | 48 | context "sanity" do 49 | with_env "http_proxy" => nil 50 | 51 | it "should have a nil http proxy uri" do 52 | expect(env.http_proxy_uri).to be_nil 53 | end 54 | end 55 | 56 | context "with a complex proxy" do 57 | with_env "http_proxy" => "admin:secret@example.com" 58 | 59 | it "should have the expcted http proxy uri" do 60 | expect(env.http_proxy_uri).to eq URI("http://admin:secret@example.com") 61 | end 62 | 63 | it "should have the expected host" do 64 | expect(env.http_proxy_uri.host).to eq "example.com" 65 | end 66 | 67 | it "should have the expected user" do 68 | expect(env.http_proxy_uri.user).to eq "admin" 69 | end 70 | 71 | it "should have the expected password" do 72 | expect(env.http_proxy_uri.password).to eq "secret" 73 | end 74 | end 75 | 76 | context "with a split proxy" do 77 | with_env "http_proxy" => "example.com", 78 | "http_proxy_user" => "admin", 79 | "http_proxy_pass" => "secret" 80 | 81 | it "should have the expcted http proxy uri" do 82 | expect(env.http_proxy_uri).to eq URI("http://admin:secret@example.com") 83 | end 84 | end 85 | 86 | end 87 | 88 | describe "#net_http_class" do 89 | let(:proxied_host) { "www.example.com" } 90 | context "sanity" do 91 | with_env "http_proxy" => nil 92 | 93 | it "should have the normal class" do 94 | expect(env.net_http_class(proxied_host)).to be Net::HTTP 95 | end 96 | 97 | it "should not be marked as a proxy class" do 98 | expect(env.net_http_class(proxied_host)).to_not be_proxy_class 99 | end 100 | end 101 | 102 | context "with a complex proxy" do 103 | with_env "http_proxy" => "admin:secret@example.com" 104 | 105 | it "should not by marked as a proxy class for localhost" do 106 | expect(env.net_http_class('localhost')).to_not be_proxy_class 107 | end 108 | it "should not have the normal class" do 109 | expect(env.net_http_class(proxied_host)).to_not be Net::HTTP 110 | end 111 | 112 | it "should have a subclass the normal class" do 113 | expect(env.net_http_class(proxied_host)).to be < Net::HTTP 114 | end 115 | 116 | it "should be marked as a proxy class" do 117 | expect(env.net_http_class(proxied_host)).to be_proxy_class 118 | end 119 | 120 | it "should have the expected proxy attributes" do 121 | http = env.net_http_class(proxied_host).new("www.kernel.org") 122 | expected_attributes = { 123 | "host" => env.http_proxy_uri.host, 124 | "port" => env.http_proxy_uri.port, 125 | "user" => env.http_proxy_uri.user, 126 | "pass" => env.http_proxy_uri.password 127 | } 128 | actual_attributes = { 129 | "host" => http.proxy_address, 130 | "port" => http.proxy_port, 131 | "user" => http.proxy_user, 132 | "pass" => http.proxy_pass, 133 | } 134 | 135 | expect(actual_attributes).to eq expected_attributes 136 | end 137 | 138 | end 139 | 140 | context "with an excluded host" do 141 | with_env "http_proxy" => "admin:secret@example.com", 142 | "no_proxy" => "no.proxy.com, noproxy.com" 143 | 144 | context "with an exact match" do 145 | let(:proxied_host) { "noproxy.com" } 146 | 147 | it "should have the normal class" do 148 | expect(env.net_http_class(proxied_host)).to be Net::HTTP 149 | end 150 | 151 | it "should not be marked as a proxy class" do 152 | expect(env.net_http_class(proxied_host)).to_not be_proxy_class 153 | end 154 | end 155 | 156 | context "with a subdomain match" do 157 | let(:proxied_host) { "www.noproxy.com" } 158 | 159 | it "should have the normal class" do 160 | expect(env.net_http_class(proxied_host)).to be Net::HTTP 161 | end 162 | 163 | it "should not be marked as a proxy class" do 164 | expect(env.net_http_class(proxied_host)).to_not be_proxy_class 165 | end 166 | end 167 | 168 | context "with localhost" do 169 | let(:proxied_host) { "localhost" } 170 | 171 | it "should have the normal class" do 172 | expect(env.net_http_class(proxied_host)).to be Net::HTTP 173 | end 174 | 175 | it "should not be marked as a proxy class" do 176 | expect(env.net_http_class(proxied_host)).to_not be_proxy_class 177 | end 178 | end 179 | 180 | context "with 127.0.0.1" do 181 | let(:proxied_host) { "127.0.0.1" } 182 | 183 | it "should have the normal class" do 184 | expect(env.net_http_class(proxied_host)).to be Net::HTTP 185 | end 186 | 187 | it "should not be marked as a proxy class" do 188 | expect(env.net_http_class(proxied_host)).to_not be_proxy_class 189 | end 190 | end 191 | 192 | context "with a mismatch" do 193 | let(:proxied_host) { "www.example.com" } 194 | 195 | it "should have a subclass the normal class" do 196 | expect(env.net_http_class(proxied_host)).to be < Net::HTTP 197 | end 198 | 199 | it "should be marked as a proxy class" do 200 | expect(env.net_http_class(proxied_host)).to be_proxy_class 201 | end 202 | end 203 | 204 | end 205 | 206 | end 207 | 208 | end 209 | end 210 | -------------------------------------------------------------------------------- /lib/librarian/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'thor/actions' 3 | 4 | require 'librarian' 5 | require 'librarian/error' 6 | require 'librarian/action' 7 | require "librarian/ui" 8 | 9 | module Librarian 10 | class Cli < Thor 11 | 12 | autoload :ManifestPresenter, "librarian/cli/manifest_presenter" 13 | 14 | include Thor::Actions 15 | 16 | module Particularity 17 | def root_module 18 | nil 19 | end 20 | end 21 | 22 | extend Particularity 23 | 24 | class << self 25 | def bin! 26 | status = with_environment { returning_status { start } } 27 | exit status 28 | end 29 | 30 | def returning_status 31 | yield 32 | 0 33 | rescue Librarian::Error => e 34 | environment.ui.error e.message 35 | environment.ui.debug e.backtrace.join("\n") 36 | e.respond_to?(:status_code) && e.status_code || 1 37 | rescue Interrupt => e 38 | environment.ui.error "\nQuitting..." 39 | 1 40 | end 41 | 42 | attr_accessor :environment 43 | 44 | def with_environment 45 | environment = root_module.environment_class.new 46 | self.environment, orig_environment = environment, self.environment 47 | yield(environment) 48 | ensure 49 | self.environment = orig_environment 50 | end 51 | end 52 | 53 | def initialize(*) 54 | super 55 | the_shell = (options["no-color"] ? Thor::Shell::Basic.new : shell) 56 | environment.ui = UI::Shell.new(the_shell) 57 | environment.ui.be_quiet! if options["quiet"] 58 | environment.ui.debug! if options["verbose"] 59 | environment.ui.debug_line_numbers! if options["verbose"] && options["line-numbers"] 60 | 61 | write_debug_header 62 | end 63 | 64 | desc "version", "Displays the version." 65 | def version 66 | say "librarian-#{environment.version}" 67 | say "librarian-#{environment.adapter_name}-#{environment.adapter_version}" 68 | end 69 | 70 | desc "config", "Show or edit the config." 71 | option "verbose", :type => :boolean, :default => false 72 | option "line-numbers", :type => :boolean, :default => false 73 | option "global", :type => :boolean, :default => false 74 | option "local", :type => :boolean, :default => false 75 | option "delete", :type => :boolean, :default => false 76 | def config(key = nil, value = nil) 77 | if key 78 | raise Error, "cannot set both value and delete" if value && options["delete"] 79 | if options["delete"] 80 | scope = config_scope(true) 81 | environment.config_db[key, scope] = nil 82 | elsif value 83 | scope = config_scope(true) 84 | environment.config_db[key, scope] = value 85 | else 86 | scope = config_scope(false) 87 | if value = environment.config_db[key, scope] 88 | prefix = scope ? "#{key} (#{scope})" : key 89 | say "#{prefix}: #{value}" 90 | end 91 | end 92 | else 93 | environment.config_db.keys.each do |key| 94 | say "#{key}: #{environment.config_db[key]}" 95 | end 96 | end 97 | end 98 | 99 | desc "clean", "Cleans out the cache and install paths." 100 | option "verbose", :type => :boolean, :default => false 101 | option "line-numbers", :type => :boolean, :default => false 102 | def clean 103 | ensure! 104 | clean! 105 | end 106 | 107 | desc "update", "Updates and installs the dependencies you specify." 108 | option "verbose", :type => :boolean, :default => false 109 | option "line-numbers", :type => :boolean, :default => false 110 | def update(*names) 111 | ensure! 112 | if names.empty? 113 | resolve!(:force => true) 114 | else 115 | update!(:names => names) 116 | end 117 | install! 118 | end 119 | 120 | desc "outdated", "Lists outdated dependencies." 121 | option "verbose", :type => :boolean, :default => false 122 | option "line-numbers", :type => :boolean, :default => false 123 | def outdated 124 | ensure! 125 | resolution = environment.lock 126 | manifests = resolution.manifests.sort_by(&:name) 127 | manifests.select(&:outdated?).each do |manifest| 128 | say "#{manifest.name} (#{manifest.version} -> #{manifest.latest.version})" 129 | end 130 | end 131 | 132 | desc "show", "Shows dependencies" 133 | option "verbose", :type => :boolean, :default => false 134 | option "line-numbers", :type => :boolean, :default => false 135 | option "detailed", :type => :boolean 136 | def show(*names) 137 | ensure! 138 | if environment.lockfile_path.file? 139 | manifest_presenter.present(names, :detailed => options["detailed"]) 140 | else 141 | raise Error, "Be sure to install first!" 142 | end 143 | end 144 | 145 | desc "init", "Initializes the current directory." 146 | def init 147 | puts "Nothing to do." 148 | end 149 | 150 | private 151 | 152 | def environment 153 | self.class.environment 154 | end 155 | 156 | def ensure!(options = { }) 157 | Action::Ensure.new(environment, options).run 158 | end 159 | 160 | def clean!(options = { }) 161 | Action::Clean.new(environment, options).run 162 | end 163 | 164 | def install!(options = { }) 165 | Action::Install.new(environment, options).run 166 | end 167 | 168 | def resolve!(options = { }) 169 | Action::Resolve.new(environment, options).run 170 | end 171 | 172 | def update!(options = { }) 173 | Action::Update.new(environment, options).run 174 | end 175 | 176 | def manifest_presenter 177 | ManifestPresenter.new(self, environment.lock.manifests) 178 | end 179 | 180 | def write_debug_header 181 | debug { "Ruby Version: #{RUBY_VERSION}" } 182 | debug { "Ruby Platform: #{RUBY_PLATFORM}" } 183 | debug { "Rubinius Version: #{Rubinius::VERSION}" } if defined?(Rubinius) 184 | debug { "JRuby Version: #{JRUBY_VERSION}" } if defined?(JRUBY_VERSION) 185 | debug { "Rubygems Version: #{Gem::VERSION}" } 186 | debug { "Librarian Version: #{environment.version}" } 187 | debug { "Librarian Adapter: #{environment.adapter_name}"} 188 | debug { "Librarian Adapter Version: #{environment.adapter_version}" } 189 | debug { "Project: #{environment.project_path}" } 190 | debug { "Specfile: #{relative_path_to(environment.specfile_path)}" } 191 | debug { "Lockfile: #{relative_path_to(environment.lockfile_path)}" } 192 | debug { "Git: #{Source::Git::Repository.bin}" } 193 | debug { "Git Version: #{Source::Git::Repository.git_version}" } 194 | debug { "Git Environment Variables:" } 195 | git_env = ENV.to_a.select{|(k, v)| k =~ /\AGIT/}.sort_by{|(k, v)| k} 196 | if git_env.empty? 197 | debug { " (empty)" } 198 | else 199 | git_env.each do |(k, v)| 200 | debug { " #{k}=#{v}"} 201 | end 202 | end 203 | end 204 | 205 | def debug(*args, &block) 206 | environment.logger.debug(*args, &block) 207 | end 208 | 209 | def relative_path_to(path) 210 | environment.logger.relative_path_to(path) 211 | end 212 | 213 | def config_scope(exclusive) 214 | g, l = "global", "local" 215 | if exclusive 216 | options[g] ^ options[l] or raise Error, "must set either #{g} or #{l}" 217 | else 218 | options[g] && options[l] and raise Error, "cannot set both #{g} and #{l}" 219 | end 220 | 221 | options[g] ? :global : options[l] ? :local : nil 222 | end 223 | 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /lib/librarian/resolver/implementation.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | require 'librarian/algorithms' 4 | require 'librarian/dependency' 5 | 6 | module Librarian 7 | class Resolver 8 | class Implementation 9 | 10 | class MultiSource 11 | attr_accessor :sources 12 | def initialize(sources) 13 | self.sources = sources 14 | end 15 | def manifests(name) 16 | sources.reverse.map{|source| source.manifests(name)}.flatten(1).compact 17 | end 18 | def to_s 19 | "(no source specified)" 20 | end 21 | end 22 | 23 | class State 24 | attr_accessor :manifests, :dependencies, :queue 25 | private :manifests=, :dependencies=, :queue= 26 | def initialize(manifests, dependencies, queue) 27 | self.manifests = manifests 28 | self.dependencies = dependencies # resolved 29 | self.queue = queue # scheduled 30 | end 31 | end 32 | 33 | attr_accessor :resolver, :spec, :cyclic 34 | private :resolver=, :spec=, :cyclic= 35 | 36 | def initialize(resolver, spec, options = { }) 37 | unrecognized_options = options.keys - [:cyclic] 38 | unrecognized_options.empty? or raise Error, 39 | "unrecognized options: #{unrecognized_options.join(", ")}" 40 | self.resolver = resolver 41 | self.spec = spec 42 | self.cyclic = !!options[:cyclic] 43 | @level = 0 44 | end 45 | 46 | def resolve(manifests) 47 | manifests = index_by(manifests, &:name) if manifests.kind_of?(Array) 48 | queue = spec.dependencies + sourced_dependencies_for_manifests(manifests) 49 | state = State.new(manifests.dup, [], queue) 50 | recursive_resolve(state) 51 | end 52 | 53 | private 54 | 55 | def recursive_resolve(state) 56 | shift_resolved_enqueued_dependencies(state) or return 57 | state.queue.empty? and return state.manifests 58 | 59 | state.dependencies << state.queue.shift 60 | dependency = state.dependencies.last 61 | 62 | resolving_dependency_map_find_manifests(dependency) do |manifest| 63 | check_manifest(state, manifest) or next 64 | check_manifest_for_cycles(state, manifest) or next unless cyclic 65 | 66 | m = state.manifests.merge(dependency.name => manifest) 67 | a = sourced_dependencies_for_manifest(manifest) 68 | s = State.new(m, state.dependencies.dup, state.queue + a) 69 | 70 | recursive_resolve(s) 71 | end 72 | end 73 | 74 | def find_inconsistency(state, dependency) 75 | m = state.manifests[dependency.name] 76 | dependency.satisfied_by?(m) or return m if m 77 | violation = lambda{|d| !dependency.consistent_with?(d)} 78 | state.dependencies.find(&violation) || state.queue.find(&violation) 79 | end 80 | 81 | # When using this method, you are required to check the return value. 82 | # Returns +true+ if the resolved enqueued dependencies at the front of the 83 | # queue could all be moved to the resolved dependencies list. 84 | # Returns +false+ if there was an inconsistency when trying to move one or 85 | # more of them. 86 | # This modifies +queue+ and +dependencies+. 87 | def shift_resolved_enqueued_dependencies(state) 88 | while (d = state.queue.first) && state.manifests[d.name] 89 | if q = find_inconsistency(state, d) 90 | debug_conflict d, q 91 | return false 92 | end 93 | state.dependencies << state.queue.shift 94 | end 95 | true 96 | end 97 | 98 | # When using this method, you are required to check the return value. 99 | # Returns +true+ if the manifest satisfies all of the dependencies. 100 | # Returns +false+ if there was a dependency that the manifest does not 101 | # satisfy. 102 | def check_manifest(state, manifest) 103 | violation = lambda{|d| d.name == manifest.name && !d.satisfied_by?(manifest)} 104 | if q = state.dependencies.find(&violation) || state.queue.find(&violation) 105 | debug_conflict manifest, q 106 | return false 107 | end 108 | true 109 | end 110 | 111 | # When using this method, you are required to check the return value. 112 | # Returns +true+ if the manifest does not introduce a cycle. 113 | # Returns +false+ if the manifest introduces a cycle. 114 | def check_manifest_for_cycles(state, manifest) 115 | manifests = state.manifests.merge(manifest.name => manifest) 116 | known = manifests.keys 117 | graph = Hash[manifests.map{|n, m| [n, m.dependencies.map(&:name) & known]}] 118 | if Algorithms::AdjacencyListDirectedGraph.cyclic?(graph) 119 | debug_cycle manifest 120 | return false 121 | end 122 | true 123 | end 124 | 125 | def default_source 126 | @default_source ||= MultiSource.new(spec.sources) 127 | end 128 | 129 | def dependency_source_map 130 | @dependency_source_map ||= 131 | Hash[spec.dependencies.map{|d| [d.name, d.source]}] 132 | end 133 | 134 | def sourced_dependency_for(dependency) 135 | return dependency if dependency.source 136 | 137 | source = dependency_source_map[dependency.name] || default_source 138 | Dependency.new(dependency.name, dependency.requirement, source) 139 | end 140 | 141 | def sourced_dependencies_for_manifest(manifest) 142 | manifest.dependencies.map{|d| sourced_dependency_for(d)} 143 | end 144 | 145 | def sourced_dependencies_for_manifests(manifests) 146 | manifests = manifests.values if manifests.kind_of?(Hash) 147 | manifests.map{|m| sourced_dependencies_for_manifest(m)}.flatten(1) 148 | end 149 | 150 | def resolving_dependency_map_find_manifests(dependency) 151 | scope_resolving_dependency dependency do 152 | map_find(dependency.manifests) do |manifest| 153 | scope_checking_manifest dependency, manifest do 154 | yield manifest 155 | end 156 | end 157 | end 158 | end 159 | 160 | def scope_resolving_dependency(dependency) 161 | debug { "Resolving #{dependency}" } 162 | resolution = nil 163 | scope do 164 | scope_checking_manifests do 165 | resolution = yield 166 | end 167 | if resolution 168 | debug { "Resolved #{dependency}" } 169 | else 170 | debug { "Failed to resolve #{dependency}" } 171 | end 172 | end 173 | resolution 174 | end 175 | 176 | def scope_checking_manifests 177 | debug { "Checking manifests" } 178 | scope do 179 | yield 180 | end 181 | end 182 | 183 | def scope_checking_manifest(dependency, manifest) 184 | debug { "Checking #{manifest}" } 185 | resolution = nil 186 | scope do 187 | resolution = yield 188 | if resolution 189 | debug { "Resolved #{dependency} at #{manifest}" } 190 | else 191 | debug { "Backtracking from #{manifest}" } 192 | end 193 | end 194 | resolution 195 | end 196 | 197 | def debug_schedule(dependency) 198 | debug { "Scheduling #{dependency}" } 199 | end 200 | 201 | def debug_conflict(dependency, conflict) 202 | debug { "Conflict between #{dependency} and #{conflict}" } 203 | end 204 | 205 | def debug_cycle(manifest) 206 | debug { "Cycle with #{manifest}" } 207 | end 208 | 209 | def map_find(enum) 210 | enum.each do |obj| 211 | res = yield(obj) 212 | res.nil? or return res 213 | end 214 | nil 215 | end 216 | 217 | def index_by(enum) 218 | Hash[enum.map{|obj| [yield(obj), obj]}] 219 | end 220 | 221 | def scope 222 | @level += 1 223 | yield 224 | ensure 225 | @level -= 1 226 | end 227 | 228 | def debug 229 | environment.logger.debug { ' ' * @level + yield } 230 | end 231 | 232 | def environment 233 | resolver.environment 234 | end 235 | 236 | end 237 | end 238 | end 239 | -------------------------------------------------------------------------------- /spec/unit/resolver_spec.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | require "tmpdir" 3 | 4 | require "support/fakefs" 5 | 6 | require 'librarian/resolver' 7 | require 'librarian/spec_change_set' 8 | require 'librarian/mock' 9 | 10 | module Librarian 11 | describe Resolver do 12 | include ::Support::FakeFS 13 | 14 | let(:env) { Mock::Environment.new } 15 | let(:resolver) { env.resolver } 16 | 17 | context "a simple specfile" do 18 | 19 | before do 20 | env.registry :clear => true do 21 | source 'source-1' do 22 | spec 'butter', '1.1' 23 | end 24 | end 25 | end 26 | 27 | let(:spec) do 28 | env.dsl do 29 | src 'source-1' 30 | dep 'butter' 31 | end 32 | end 33 | 34 | let(:resolution) { resolver.resolve(spec) } 35 | 36 | specify { expect(resolution).to be_correct } 37 | 38 | end 39 | 40 | context "a specfile with a dep from one src depending on a dep from another src" do 41 | 42 | before do 43 | env.registry :clear => true do 44 | source 'source-1' do 45 | spec 'butter', '1.1' 46 | end 47 | source 'source-2' do 48 | spec 'jam', '1.2' do 49 | dependency 'butter', '>= 1.0' 50 | end 51 | end 52 | end 53 | end 54 | 55 | let(:spec) do 56 | env.dsl do 57 | src 'source-1' 58 | src 'source-2' do 59 | dep 'jam' 60 | end 61 | end 62 | end 63 | 64 | let(:resolution) { resolver.resolve(spec) } 65 | 66 | specify { expect(resolution).to be_correct } 67 | 68 | end 69 | 70 | context "a specfile with a dep in multiple sources" do 71 | 72 | before do 73 | env.registry :clear => true do 74 | source 'source-1' do 75 | spec 'butter', '1.0' 76 | spec 'butter', '1.1' 77 | end 78 | source 'source-2' do 79 | spec 'butter', '1.0' 80 | spec 'butter', '1.1' 81 | end 82 | source 'source-3' do 83 | spec 'butter', '1.0' 84 | end 85 | end 86 | end 87 | 88 | let(:spec) do 89 | env.dsl do 90 | src 'source-1' 91 | src 'source-2' 92 | dep 'butter', '>= 1.1' 93 | end 94 | end 95 | 96 | it "should have the expected number of sources" do 97 | expect(spec).to have(2).sources 98 | end 99 | 100 | let(:resolution) { resolver.resolve(spec) } 101 | 102 | specify { expect(resolution).to be_correct } 103 | 104 | it "should have the manifest from the final source with a matching manifest" do 105 | manifest = resolution.manifests.find{|m| m.name == "butter"} 106 | expect(manifest.source.name).to eq "source-2" 107 | end 108 | 109 | end 110 | 111 | context "a specfile with a dep depending on a nonexistent dep" do 112 | 113 | before do 114 | env.registry :clear => true do 115 | source 'source-1' do 116 | spec 'jam', '1.2' do 117 | dependency 'butter', '>= 1.0' 118 | end 119 | end 120 | end 121 | end 122 | 123 | let(:spec) do 124 | env.dsl do 125 | src 'source-1' 126 | dep 'jam' 127 | end 128 | end 129 | 130 | let(:resolution) { resolver.resolve(spec) } 131 | 132 | specify { expect(resolution).to be_nil } 133 | 134 | end 135 | 136 | context "a specfile with conflicting constraints" do 137 | 138 | before do 139 | env.registry :clear => true do 140 | source 'source-1' do 141 | spec 'butter', '1.0' 142 | spec 'butter', '1.1' 143 | spec 'jam', '1.2' do 144 | dependency 'butter', '1.1' 145 | end 146 | end 147 | end 148 | end 149 | 150 | let(:spec) do 151 | env.dsl do 152 | src 'source-1' 153 | dep 'butter', '1.0' 154 | dep 'jam' 155 | end 156 | end 157 | 158 | let(:resolution) { resolver.resolve(spec) } 159 | 160 | specify { expect(resolution).to be_nil } 161 | 162 | end 163 | 164 | context "a specfile with cyclic constraints" do 165 | 166 | before do 167 | env.registry :clear => true do 168 | source 'source-1' do 169 | spec 'butter', '1.0' do 170 | dependency 'jam', '2.0' 171 | end 172 | spec 'jam', '2.0' do 173 | dependency 'butter', '1.0' 174 | end 175 | end 176 | end 177 | end 178 | 179 | let(:spec) do 180 | env.dsl do 181 | src 'source-1' 182 | dep 'butter' 183 | end 184 | end 185 | 186 | let(:resolution) { resolver.resolve(spec) } 187 | 188 | context "when cyclic resolutions are forbidden" do 189 | let(:resolver) { env.resolver(:cyclic => false) } 190 | 191 | specify { expect(resolution).to be_nil } 192 | end 193 | 194 | context "when cyclic resolutions are permitted" do 195 | let(:resolver) { env.resolver(:cyclic => true) } 196 | 197 | it "should have all the manifests" do 198 | manifest_names = resolution.manifests.map(&:name).sort 199 | expect(manifest_names).to be == %w[butter jam] 200 | end 201 | end 202 | 203 | end 204 | 205 | context "updating" do 206 | 207 | it "should not work" do 208 | env.registry :clear => true do 209 | source 'source-1' do 210 | spec 'butter', '1.0' 211 | spec 'butter', '1.1' 212 | spec 'jam', '1.2' do 213 | dependency 'butter' 214 | end 215 | end 216 | end 217 | first_spec = env.dsl do 218 | src 'source-1' 219 | dep 'butter', '1.1' 220 | dep 'jam' 221 | end 222 | first_resolution = resolver.resolve(first_spec) 223 | expect(first_resolution).to be_correct 224 | first_manifests = first_resolution.manifests 225 | first_manifests_index = Hash[first_manifests.map{|m| [m.name, m]}] 226 | expect(first_manifests_index['butter'].version.to_s).to eq '1.1' 227 | 228 | second_spec = env.dsl do 229 | src 'source-1' 230 | dep 'butter', '1.0' 231 | dep 'jam' 232 | end 233 | locked_manifests = ManifestSet.deep_strip(first_manifests, ['butter']) 234 | second_resolution =resolver.resolve(second_spec, locked_manifests) 235 | expect(second_resolution).to be_correct 236 | second_manifests = second_resolution.manifests 237 | second_manifests_index = Hash[second_manifests.map{|m| [m.name, m]}] 238 | expect(second_manifests_index['butter'].version.to_s).to eq '1.0' 239 | end 240 | 241 | end 242 | 243 | context "a change to the spec" do 244 | 245 | it "should work" do 246 | env.registry :clear => true do 247 | source 'source-1' do 248 | spec 'butter', '1.0' 249 | end 250 | source 'source-2' do 251 | spec 'butter', '1.0' 252 | end 253 | end 254 | spec = env.dsl do 255 | src 'source-1' 256 | dep 'butter' 257 | end 258 | lock = resolver.resolve(spec) 259 | expect(lock).to be_correct 260 | 261 | spec = env.dsl do 262 | src 'source-1' 263 | dep 'butter', :src => 'source-2' 264 | end 265 | changes = SpecChangeSet.new(env, spec, lock) 266 | expect(changes).to_not be_same 267 | manifests = ManifestSet.new(changes.analyze).to_hash 268 | expect(manifests).to_not have_key('butter') 269 | lock = resolver.resolve(spec, changes.analyze) 270 | expect(lock).to be_correct 271 | expect(lock.manifests.map{|m| m.name}).to include('butter') 272 | manifest = lock.manifests.find{|m| m.name == 'butter'} 273 | expect(manifest).to_not be_nil 274 | expect(manifest.source.name).to eq 'source-2' 275 | end 276 | 277 | end 278 | 279 | context "a pathname to a simple specfile" do 280 | let(:pwd) { Pathname(Dir.tmpdir) } 281 | let(:specfile_path) { pwd + "Mockfile" } 282 | before { FileUtils.mkpath(pwd) } 283 | 284 | def write!(path, text) 285 | Pathname(path).open("wb"){|f| f.write(text)} 286 | end 287 | 288 | it "loads the specfile with the __FILE__" do 289 | write! specfile_path, "src __FILE__" 290 | spec = env.dsl(specfile_path) 291 | expect(spec.sources).to have(1).item 292 | source = spec.sources.first 293 | expect(source.name).to eq specfile_path.to_s 294 | end 295 | 296 | end 297 | 298 | end 299 | end 300 | --------------------------------------------------------------------------------