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.
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