elm_install 0.3.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.reek +4 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +4 -0
  5. data/Gemfile.lock +30 -25
  6. data/Rakefile +2 -2
  7. data/Readme.md +85 -23
  8. data/bin/elm-install +5 -2
  9. data/elm_install.gemspec +2 -0
  10. data/lib/elm_install.rb +18 -1
  11. data/lib/elm_install/base.rb +3 -46
  12. data/lib/elm_install/dependency.rb +37 -0
  13. data/lib/elm_install/directory_source.rb +66 -0
  14. data/lib/elm_install/ext.rb +23 -0
  15. data/lib/elm_install/git_source.rb +173 -0
  16. data/lib/elm_install/identifier.rb +133 -0
  17. data/lib/elm_install/installer.rb +38 -96
  18. data/lib/elm_install/populator.rb +54 -75
  19. data/lib/elm_install/repository.rb +82 -0
  20. data/lib/elm_install/resolver.rb +48 -118
  21. data/lib/elm_install/source.rb +18 -0
  22. data/lib/elm_install/types.rb +43 -0
  23. data/lib/elm_install/utils.rb +14 -20
  24. data/lib/elm_install/version.rb +1 -1
  25. data/package.json +1 -1
  26. data/packaging/Gemfile +1 -1
  27. data/packaging/Gemfile.lock +8 -4
  28. data/scripts/install.js +4 -4
  29. data/scripts/run.js +4 -4
  30. data/spec/directory_source_spec.rb +37 -0
  31. data/spec/{eml_install_spec.rb → elm_install_spec.rb} +6 -3
  32. data/spec/git_source_spec.rb +115 -0
  33. data/spec/identifer_spec.rb +53 -0
  34. data/spec/installer_spec.rb +57 -26
  35. data/spec/repository_spec.rb +44 -0
  36. data/spec/resolver_spec.rb +0 -73
  37. data/spec/spec_helper.rb +3 -1
  38. data/spec/utils_spec.rb +10 -15
  39. metadata +43 -17
  40. data/docs/How it works.md +0 -54
  41. data/lib/elm_install/cache.rb +0 -52
  42. data/lib/elm_install/elm_package.rb +0 -119
  43. data/lib/elm_install/git_resolver.rb +0 -129
  44. data/lib/elm_install/graph_builder.rb +0 -73
  45. data/spec/elm_package_spec.rb +0 -73
  46. data/spec/fixtures/cache.json +0 -8
  47. data/spec/fixtures/elm-package.json +0 -12
  48. data/spec/fixtures/invalid-elm-package.json +0 -6
  49. data/spec/fixtures/mismatched-elm-package.json +0 -9
  50. data/spec/fixtures/ref-cache.json +0 -4
  51. data/spec/git_resolver_spec.rb +0 -103
  52. data/spec/graph_builder_spec.rb +0 -36
  53. 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
- # This class is responsible getting a solution for the `elm-package.json`
9
- # file and populating the `elm-stuff` directory with the packages and
10
- # writing the `elm-stuff/exact-dependencies.json`.
11
- class Installer
12
- # Initializes a new installer with the given options.
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
- # @param options [Hash] The options
26
- #
27
- # @return [Hash] The options
28
- def init_options(options = { verbose: false })
29
- options[:cache_directory] ||= File.join(Dir.home, '.elm-install')
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
- # Returns the path to the cache directory
17
+ Contract None => NilClass
18
+ # Installs packages
34
19
  #
35
- # @return [String] The path
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.add_constraints dependencies
23
+ @graph = @resolver.resolve
48
24
 
49
25
  puts 'Solving dependencies...'
50
- populate_elm_stuff
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
- # Returns the resolver to calculate the solution.
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 [Hash] The solution
103
- def calculate_solution
104
- Solve.it!(
105
- GraphBuilder.graph_from_cache(@cache, @options),
106
- resolver.constraints
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
- # Returns the dependencies form `elm-package.json`.
48
+ Contract None => Array
49
+ # Returns the inital constraints
111
50
  #
112
- # @return [Hash] The dependencies
113
- def dependencies
114
- @dependencies ||= ElmPackage.dependencies 'elm-package.json',
115
- silent: false
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