elm_install 0.3.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.reek +4 -0
- data/.rspec +1 -0
- data/.rubocop.yml +4 -0
- data/Gemfile.lock +30 -25
- data/Rakefile +2 -2
- data/Readme.md +85 -23
- data/bin/elm-install +5 -2
- data/elm_install.gemspec +2 -0
- data/lib/elm_install.rb +18 -1
- data/lib/elm_install/base.rb +3 -46
- data/lib/elm_install/dependency.rb +37 -0
- data/lib/elm_install/directory_source.rb +66 -0
- data/lib/elm_install/ext.rb +23 -0
- data/lib/elm_install/git_source.rb +173 -0
- data/lib/elm_install/identifier.rb +133 -0
- data/lib/elm_install/installer.rb +38 -96
- data/lib/elm_install/populator.rb +54 -75
- data/lib/elm_install/repository.rb +82 -0
- data/lib/elm_install/resolver.rb +48 -118
- data/lib/elm_install/source.rb +18 -0
- data/lib/elm_install/types.rb +43 -0
- data/lib/elm_install/utils.rb +14 -20
- data/lib/elm_install/version.rb +1 -1
- data/package.json +1 -1
- data/packaging/Gemfile +1 -1
- data/packaging/Gemfile.lock +8 -4
- data/scripts/install.js +4 -4
- data/scripts/run.js +4 -4
- data/spec/directory_source_spec.rb +37 -0
- data/spec/{eml_install_spec.rb → elm_install_spec.rb} +6 -3
- data/spec/git_source_spec.rb +115 -0
- data/spec/identifer_spec.rb +53 -0
- data/spec/installer_spec.rb +57 -26
- data/spec/repository_spec.rb +44 -0
- data/spec/resolver_spec.rb +0 -73
- data/spec/spec_helper.rb +3 -1
- data/spec/utils_spec.rb +10 -15
- metadata +43 -17
- data/docs/How it works.md +0 -54
- data/lib/elm_install/cache.rb +0 -52
- data/lib/elm_install/elm_package.rb +0 -119
- data/lib/elm_install/git_resolver.rb +0 -129
- data/lib/elm_install/graph_builder.rb +0 -73
- data/spec/elm_package_spec.rb +0 -73
- data/spec/fixtures/cache.json +0 -8
- data/spec/fixtures/elm-package.json +0 -12
- data/spec/fixtures/invalid-elm-package.json +0 -6
- data/spec/fixtures/mismatched-elm-package.json +0 -9
- data/spec/fixtures/ref-cache.json +0 -4
- data/spec/git_resolver_spec.rb +0 -103
- data/spec/graph_builder_spec.rb +0 -36
- data/spec/populator_spec.rb +0 -44
@@ -0,0 +1,66 @@
|
|
1
|
+
module ElmInstall
|
2
|
+
# This clas handles sources that point to a local directory.
|
3
|
+
class DirectorySource < Source
|
4
|
+
# @return [Dir] The directory
|
5
|
+
attr_reader :dir
|
6
|
+
|
7
|
+
Contract Pathname => DirectorySource
|
8
|
+
# Initializes a directory source with the given directory.
|
9
|
+
#
|
10
|
+
# @param dir [Dir] The directory
|
11
|
+
#
|
12
|
+
# @return [DirectorySource] The directory source instance
|
13
|
+
def initialize(dir)
|
14
|
+
@dir = dir
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
Contract Or[String, Semverse::Version] => Dir
|
19
|
+
# Returns the directory
|
20
|
+
#
|
21
|
+
# @param _ [String] The version
|
22
|
+
#
|
23
|
+
# @return [Dir] The directory
|
24
|
+
def fetch(_)
|
25
|
+
Dir.new(@dir.expand_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
Contract Semverse::Version, Pathname => nil
|
29
|
+
# Copies the directory to the given other directory
|
30
|
+
#
|
31
|
+
# @param _ [Semverse::Version] The version
|
32
|
+
# @param directory [Pathname] The pathname
|
33
|
+
#
|
34
|
+
# @return nil
|
35
|
+
def copy_to(_, directory)
|
36
|
+
# Delete the directory to make sure no pervious version remains
|
37
|
+
FileUtils.rm_rf(directory) if directory.exist?
|
38
|
+
|
39
|
+
# Create parent directory
|
40
|
+
FileUtils.mkdir_p(directory.parent)
|
41
|
+
|
42
|
+
# Create symlink
|
43
|
+
FileUtils.ln_s(@dir.expand_path, directory)
|
44
|
+
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
Contract ArrayOf[Solve::Constraint] => ArrayOf[Semverse::Version]
|
49
|
+
# Returns the available versions for a repository
|
50
|
+
#
|
51
|
+
# @param _ [Array] The constraints
|
52
|
+
#
|
53
|
+
# @return [Array] The versions
|
54
|
+
def versions(_)
|
55
|
+
[identifier.version(fetch(''))]
|
56
|
+
end
|
57
|
+
|
58
|
+
Contract None => String
|
59
|
+
# Returns the log format
|
60
|
+
#
|
61
|
+
# @return [String]
|
62
|
+
def to_log
|
63
|
+
@dir.expand_path.to_s
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Extensions for the semvese module
|
2
|
+
module Semverse
|
3
|
+
# Added utility functions
|
4
|
+
class Version
|
5
|
+
# Returns the simple string representation of a version
|
6
|
+
#
|
7
|
+
# @return [String]
|
8
|
+
def to_simple
|
9
|
+
"#{major}.#{minor}.#{patch}"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Tries to parse a version, falling back to nil if fails.
|
13
|
+
#
|
14
|
+
# @param version [String] The version to parse
|
15
|
+
#
|
16
|
+
# @return [Semverse::Version]
|
17
|
+
def self.try_new(version)
|
18
|
+
new version
|
19
|
+
rescue
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
module ElmInstall
|
2
|
+
# Git Source
|
3
|
+
class GitSource < Source
|
4
|
+
# @return [Uri] The uri
|
5
|
+
attr_reader :uri
|
6
|
+
|
7
|
+
# @return [Branch] The branch
|
8
|
+
attr_reader :branch
|
9
|
+
|
10
|
+
Contract Uri, Branch => GitSource
|
11
|
+
# Initializes a git source by URI and branch
|
12
|
+
#
|
13
|
+
# @param uri [Uri] The uri
|
14
|
+
# @param branch [Branch] The branch
|
15
|
+
#
|
16
|
+
# @return [GitSource]
|
17
|
+
def initialize(uri, branch)
|
18
|
+
@branch = branch
|
19
|
+
@uri = uri
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
Contract Or[String, Semverse::Version] => Dir
|
24
|
+
# Downloads the version into a temporary directory
|
25
|
+
#
|
26
|
+
# @param version [Semverse::Version] The version to fetch
|
27
|
+
#
|
28
|
+
# @return [Dir] The directory for the source of the version
|
29
|
+
def fetch(version)
|
30
|
+
# Get the reference from the branch
|
31
|
+
ref =
|
32
|
+
case @branch
|
33
|
+
when Branch::Just
|
34
|
+
@branch.ref
|
35
|
+
when Branch::Nothing
|
36
|
+
version.to_simple
|
37
|
+
end
|
38
|
+
|
39
|
+
repository.checkout ref
|
40
|
+
end
|
41
|
+
|
42
|
+
Contract Semverse::Version, Pathname => nil
|
43
|
+
# Copies the version into the given directory
|
44
|
+
#
|
45
|
+
# @param version [Semverse::Version] The version
|
46
|
+
# @param directory [Pathname] The pathname
|
47
|
+
#
|
48
|
+
# @return nil
|
49
|
+
def copy_to(version, directory)
|
50
|
+
# Delete the directory to make sure no pervious version remains if
|
51
|
+
# we are using a branch or symlink if using Dir.
|
52
|
+
FileUtils.rm_rf(directory) if directory.exist?
|
53
|
+
|
54
|
+
# Create directory if not exists
|
55
|
+
FileUtils.mkdir_p directory
|
56
|
+
|
57
|
+
# Copy hole repository
|
58
|
+
FileUtils.cp_r("#{fetch(version).path}/.", directory)
|
59
|
+
|
60
|
+
# Remove .git directory
|
61
|
+
FileUtils.rm_rf(File.join(directory, '.git'))
|
62
|
+
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
Contract ArrayOf[Solve::Constraint] => ArrayOf[Semverse::Version]
|
67
|
+
# Returns the available versions for a repository
|
68
|
+
#
|
69
|
+
# @param constraints [Array] The constraints
|
70
|
+
#
|
71
|
+
# @return [Array] The versions
|
72
|
+
def versions(constraints)
|
73
|
+
case @branch
|
74
|
+
when Branch::Just
|
75
|
+
[identifier.version(fetch(@branch.ref))]
|
76
|
+
when Branch::Nothing
|
77
|
+
matches = matching_versions constraints
|
78
|
+
return matches if matches.any?
|
79
|
+
Logger.arrow(
|
80
|
+
"Could not find matching versions for: #{package_name.bold}"\
|
81
|
+
' in cache. Fetching updates.'
|
82
|
+
)
|
83
|
+
repository.fetch
|
84
|
+
matching_versions constraints
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
Contract ArrayOf[Solve::Constraint] => ArrayOf[Semverse::Version]
|
89
|
+
# Returns the matchign versions for a repository for the given constraints
|
90
|
+
#
|
91
|
+
# @param constraints [Array] The constraints
|
92
|
+
#
|
93
|
+
# @return [Array] The versions
|
94
|
+
def matching_versions(constraints)
|
95
|
+
repository
|
96
|
+
.versions
|
97
|
+
.select do |version|
|
98
|
+
constraints.all? { |constraint| constraint.satisfies?(version) }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
Contract None => String
|
103
|
+
# Returns the url for the repository
|
104
|
+
#
|
105
|
+
# @return [String] The url
|
106
|
+
def url
|
107
|
+
@uri.to_s
|
108
|
+
end
|
109
|
+
|
110
|
+
Contract None => String
|
111
|
+
# Returns the temporary path for the repository
|
112
|
+
#
|
113
|
+
# @return [String] The path
|
114
|
+
def path
|
115
|
+
File.join(options[:cache_directory].to_s, host, package_name)
|
116
|
+
end
|
117
|
+
|
118
|
+
Contract None => String
|
119
|
+
# Returns the host for the repository
|
120
|
+
#
|
121
|
+
# @return [String] The host
|
122
|
+
def host
|
123
|
+
case @uri
|
124
|
+
when Uri::Github
|
125
|
+
'github.com'
|
126
|
+
else
|
127
|
+
@uri.uri.host
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
Contract None => String
|
132
|
+
# Returns the package name for the repository
|
133
|
+
#
|
134
|
+
# @return [String] The name
|
135
|
+
def package_name
|
136
|
+
case @uri
|
137
|
+
when Uri::Github
|
138
|
+
@uri.name
|
139
|
+
else
|
140
|
+
@uri.uri.path.sub(%r{^/}, '')
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
Contract None => Repository
|
145
|
+
# Returns the local repository
|
146
|
+
#
|
147
|
+
# @return [Repository] The repository
|
148
|
+
def repository
|
149
|
+
@repository ||= Repository.new url, path
|
150
|
+
end
|
151
|
+
|
152
|
+
Contract None => Or[String, NilClass]
|
153
|
+
# Returns the log format
|
154
|
+
#
|
155
|
+
# @return [String]
|
156
|
+
def to_log
|
157
|
+
case @uri
|
158
|
+
when Uri::Ssh, Uri::Http
|
159
|
+
case @branch
|
160
|
+
when Branch::Just
|
161
|
+
"#{url} at #{@branch.ref}"
|
162
|
+
else
|
163
|
+
# NOTE: Cannot happen
|
164
|
+
# :nocov:
|
165
|
+
url
|
166
|
+
# :nocov:
|
167
|
+
end
|
168
|
+
else
|
169
|
+
url
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module ElmInstall
|
2
|
+
# Identifies dependencies
|
3
|
+
class Identifier < Base
|
4
|
+
# @return [Array<Dependency>] The initial dependencies
|
5
|
+
attr_reader :initial_dependencies
|
6
|
+
|
7
|
+
# @return [Hash] The options
|
8
|
+
attr_reader :options
|
9
|
+
|
10
|
+
Contract Dir, Hash => Identifier
|
11
|
+
# Initialize a new identifier.
|
12
|
+
#
|
13
|
+
# @param directory [Dir] The initial directory
|
14
|
+
# @param options [Hash] The options
|
15
|
+
#
|
16
|
+
# @return [Indentifier] The identifier instance
|
17
|
+
def initialize(directory, options = {})
|
18
|
+
@options = options
|
19
|
+
@dependency_sources = dependency_sources directory
|
20
|
+
@initial_dependencies = identify directory
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
Contract Dir => HashOf[String => Any]
|
25
|
+
# Returns the dependency sources for the given directory.
|
26
|
+
#
|
27
|
+
# @param directory [Dir] The directory
|
28
|
+
#
|
29
|
+
# @return [Hash] The directory sources
|
30
|
+
def dependency_sources(directory)
|
31
|
+
json(directory)['dependency-sources'].to_h
|
32
|
+
end
|
33
|
+
|
34
|
+
Contract Dir => Semverse::Version
|
35
|
+
# Returns the version of a package in the given directory.
|
36
|
+
#
|
37
|
+
# @param directory [Dir] The directory
|
38
|
+
#
|
39
|
+
# @return [Semverse::Version] The version
|
40
|
+
def version(directory)
|
41
|
+
Semverse::Version.new(json(directory)['version'])
|
42
|
+
end
|
43
|
+
|
44
|
+
Contract Dir => ArrayOf[Dependency]
|
45
|
+
# Identifies dependencies from a directory
|
46
|
+
#
|
47
|
+
# @param directory [Dir] The directory
|
48
|
+
#
|
49
|
+
# @return [Array] The dependencies
|
50
|
+
def identify(directory)
|
51
|
+
raw = json(directory)
|
52
|
+
|
53
|
+
dependencies = raw['dependencies'].to_h
|
54
|
+
|
55
|
+
dependency_sources =
|
56
|
+
raw['dependency-sources']
|
57
|
+
.to_h
|
58
|
+
.merge(@dependency_sources)
|
59
|
+
|
60
|
+
dependencies.map do |package, constraint|
|
61
|
+
constraints = Utils.transform_constraint constraint
|
62
|
+
|
63
|
+
type =
|
64
|
+
if dependency_sources.key?(package)
|
65
|
+
source = dependency_sources[package]
|
66
|
+
case source
|
67
|
+
when Hash
|
68
|
+
uri_type source['url'], Branch::Just(source['ref'])
|
69
|
+
when String
|
70
|
+
if File.exist?(source)
|
71
|
+
Type::Directory(Pathname.new(source))
|
72
|
+
else
|
73
|
+
uri_type source, Branch::Just('master')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
else
|
77
|
+
Type::Git(Uri::Github(package), Branch::Nothing())
|
78
|
+
end
|
79
|
+
|
80
|
+
type.source.identifier = self
|
81
|
+
type.source.options = @options
|
82
|
+
|
83
|
+
Dependency.new(package, type.source, constraints)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
Contract String, Branch => Type
|
88
|
+
# Returns the type from the given arguments.
|
89
|
+
#
|
90
|
+
# @param url [String] The base url
|
91
|
+
# @param branch [Branch] The branch
|
92
|
+
#
|
93
|
+
# @return [Type] The type
|
94
|
+
def uri_type(url, branch)
|
95
|
+
uri = GitCloneUrl.parse(url)
|
96
|
+
case uri
|
97
|
+
when URI::SshGit::Generic
|
98
|
+
Type::Git(Uri::Ssh(uri), branch)
|
99
|
+
when URI::HTTP
|
100
|
+
Type::Git(Uri::Http(uri), branch)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
Contract Dir => HashOf[String => Any]
|
105
|
+
# Returns the contents of the 'elm-package.json' for the given directory.
|
106
|
+
#
|
107
|
+
# @param directory [Dir] The directory
|
108
|
+
#
|
109
|
+
# @return [Hash] The contents
|
110
|
+
def json(directory)
|
111
|
+
path = File.join(directory, 'elm-package.json')
|
112
|
+
JSON.parse(File.read(path))
|
113
|
+
rescue JSON::ParserError
|
114
|
+
exit "Invalid JSON in file: #{path.bold}"
|
115
|
+
{}
|
116
|
+
rescue Errno::ENOENT
|
117
|
+
exit "Could not find file: #{path.bold}"
|
118
|
+
{}
|
119
|
+
end
|
120
|
+
|
121
|
+
Contract String => NilClass
|
122
|
+
# Exits the current process and logs a given message.
|
123
|
+
#
|
124
|
+
# @param message [String] The message
|
125
|
+
#
|
126
|
+
# @return nil
|
127
|
+
def exit(message)
|
128
|
+
Logger.arrow message
|
129
|
+
Process.exit
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -1,118 +1,60 @@
|
|
1
|
-
require_relative './resolver'
|
2
|
-
require_relative './elm_package'
|
3
|
-
require_relative './git_resolver'
|
4
|
-
require_relative './graph_builder'
|
5
|
-
require_relative './populator'
|
6
|
-
|
7
1
|
module ElmInstall
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
# Initializes
|
2
|
+
# Installer class
|
3
|
+
class Installer < Base
|
4
|
+
Contract KeywordArgs[cache_directory: Or[String, NilClass],
|
5
|
+
verbose: Or[Bool, NilClass]] => Installer
|
6
|
+
# Initializes an installer with the given options
|
13
7
|
#
|
14
8
|
# @param options [Hash] The options
|
15
|
-
def initialize(options)
|
16
|
-
init_options options
|
17
|
-
@git_resolver = GitResolver.new directory: cache_directory
|
18
|
-
@cache = Cache.new directory: cache_directory
|
19
|
-
@populator = Populator.new @git_resolver
|
20
|
-
@options = options
|
21
|
-
end
|
22
|
-
|
23
|
-
# Initializes the options setting default values.
|
24
9
|
#
|
25
|
-
# @
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
@options = options
|
10
|
+
# @return [Installer] The installer instance
|
11
|
+
def initialize(options = {})
|
12
|
+
@identifier = Identifier.new Dir.new(Dir.pwd), options
|
13
|
+
@resolver = Resolver.new @identifier
|
14
|
+
self
|
31
15
|
end
|
32
16
|
|
33
|
-
|
17
|
+
Contract None => NilClass
|
18
|
+
# Installs packages
|
34
19
|
#
|
35
|
-
# @return
|
36
|
-
def cache_directory
|
37
|
-
@options[:cache_directory]
|
38
|
-
end
|
39
|
-
|
40
|
-
# Executes the installation
|
41
|
-
#
|
42
|
-
# :reek:TooManyStatements { max_statements: 7 }
|
43
|
-
#
|
44
|
-
# @return [void]
|
20
|
+
# @return nil
|
45
21
|
def install
|
46
22
|
puts 'Resolving packages...'
|
47
|
-
resolver.
|
23
|
+
@graph = @resolver.resolve
|
48
24
|
|
49
25
|
puts 'Solving dependencies...'
|
50
|
-
|
51
|
-
rescue
|
52
|
-
retry_install
|
53
|
-
end
|
54
|
-
|
55
|
-
# Saves the caches
|
56
|
-
#
|
57
|
-
# @return [void]
|
58
|
-
def save
|
59
|
-
puts 'Saving package cache...'
|
60
|
-
@git_resolver.save
|
61
|
-
@cache.save
|
62
|
-
end
|
26
|
+
(Populator.new results).populate
|
63
27
|
|
64
|
-
# Clears the reference cache and retries installation.
|
65
|
-
#
|
66
|
-
# @return [void]
|
67
|
-
def retry_install
|
68
|
-
Logger.arrow(
|
69
|
-
'Could not find a solution in local cache, refreshing packages...'
|
70
|
-
)
|
71
|
-
|
72
|
-
@git_resolver.clear
|
73
|
-
resolver.add_constraints dependencies
|
74
|
-
|
75
|
-
populate_elm_stuff
|
76
|
-
rescue Solve::Errors::NoSolutionError => error
|
77
|
-
puts 'Could not find a solution:'
|
78
|
-
puts error.to_s.indent(2)
|
79
|
-
end
|
80
|
-
|
81
|
-
private
|
82
|
-
|
83
|
-
# Populates the `elm-stuff` directory with the packages from
|
84
|
-
# the solution.
|
85
|
-
#
|
86
|
-
# @return [void]
|
87
|
-
def populate_elm_stuff
|
88
|
-
save
|
89
|
-
@populator.populate calculate_solution
|
90
28
|
puts 'Packages configured successfully!'
|
29
|
+
nil
|
30
|
+
rescue Solve::Errors::NoSolutionError => error
|
31
|
+
Logger.arrow "No solution found: #{error}"
|
91
32
|
end
|
92
33
|
|
93
|
-
|
94
|
-
#
|
95
|
-
# @return [Resolver] The resolver
|
96
|
-
def resolver
|
97
|
-
@resolver ||= Resolver.new @cache, @git_resolver
|
98
|
-
end
|
99
|
-
|
100
|
-
# Returns the solution for the given `elm-package.json` file.
|
34
|
+
Contract None => ArrayOf[Dependency]
|
35
|
+
# Returns the results of solving
|
101
36
|
#
|
102
|
-
# @return [
|
103
|
-
def
|
104
|
-
Solve
|
105
|
-
|
106
|
-
|
107
|
-
|
37
|
+
# @return [Array] Array of dependencies
|
38
|
+
def results
|
39
|
+
Solve
|
40
|
+
.it!(@graph, initial_solve_constraints)
|
41
|
+
.map do |name, version|
|
42
|
+
dep = @resolver.dependencies[name]
|
43
|
+
dep.version = Semverse::Version.new(version)
|
44
|
+
dep
|
45
|
+
end
|
108
46
|
end
|
109
47
|
|
110
|
-
|
48
|
+
Contract None => Array
|
49
|
+
# Returns the inital constraints
|
111
50
|
#
|
112
|
-
# @return [
|
113
|
-
def
|
114
|
-
@
|
115
|
-
|
51
|
+
# @return [Array] Array of dependency names and constraints
|
52
|
+
def initial_solve_constraints
|
53
|
+
@identifier.initial_dependencies.flat_map do |dependency|
|
54
|
+
dependency.constraints.map do |constraint|
|
55
|
+
[dependency.name, constraint]
|
56
|
+
end
|
57
|
+
end
|
116
58
|
end
|
117
59
|
end
|
118
60
|
end
|