dependabot-go_modules 0.120.3 → 0.122.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ba69f6aa10d39e7c438ea6daca7e59083639eb56ab219a750b347100408e055
4
- data.tar.gz: 5e61dbc6788267f0742040df46585e1f99ed85f2d39a5bfb35e253bcdb89392a
3
+ metadata.gz: 4668f28343a248c1d0e2ec3d61ffb59b79be51eb0df6ece487db719379935e3f
4
+ data.tar.gz: a5957a544166a39647cee725a66c85503887a8e141589adbfed2987a47d987b3
5
5
  SHA512:
6
- metadata.gz: 8956cf5e519f92b08051f41cd2cbec50568fbb8c108fecab2483807e0d34e93c77b36d9f7891ba5cc0a9612721b3a8f56f26312a06412c0a231c99b3ec0c22db
7
- data.tar.gz: '019c64586f858ec3ed1ff6bad2cbb52656bc290b8cdec09557f15c4ee3098a4976d01c5dc9d0868dac85b212639aca415811a02123bf792d3df45a11462beac5'
6
+ metadata.gz: ffe602aaff9f86aa15d94ebe13d5fd533e614eeb293819a6521403fc65cbff4158a0d4b1aa00aa1373e757ec47039f95d69b39e2d9ff386e9a42055e1e8283ef
7
+ data.tar.gz: 0b30b162084a264df70c145b1fecc99f9895e9c5594a08b2a90730dd8b1aa56c9bd535eeb6bc78430e140812dacca17a9e61017c1dfa76eebaf4ffef739b22d8
@@ -17,3 +17,6 @@ Dependabot::PullRequestCreator::Labeler.
17
17
  require "dependabot/dependency"
18
18
  Dependabot::Dependency.
19
19
  register_production_check("go_modules", ->(_) { true })
20
+
21
+ require "dependabot/utils"
22
+ Dependabot::Utils.register_always_clone("go_modules")
@@ -17,23 +17,31 @@ module Dependabot
17
17
  private
18
18
 
19
19
  def fetch_files
20
- unless go_mod
21
- raise(
22
- Dependabot::DependencyFileNotFound,
23
- File.join(directory, "go.mod")
24
- )
20
+ # Ensure we always check out the full repo contents for go_module
21
+ # updates.
22
+ SharedHelpers.in_a_temporary_repo_directory(
23
+ directory,
24
+ clone_repo_contents
25
+ ) do
26
+ unless go_mod
27
+ raise(
28
+ Dependabot::DependencyFileNotFound,
29
+ Pathname.new(File.join(directory, "go.mod")).
30
+ cleanpath.to_path
31
+ )
32
+ end
33
+
34
+ fetched_files = [go_mod]
35
+
36
+ # Fetch the (optional) go.sum
37
+ fetched_files << go_sum if go_sum
38
+
39
+ # Fetch the main.go file if present, as this will later identify
40
+ # this repo as an app.
41
+ fetched_files << main if main
42
+
43
+ fetched_files
25
44
  end
26
-
27
- fetched_files = [go_mod]
28
-
29
- # Fetch the (optional) go.sum
30
- fetched_files << go_sum if go_sum
31
-
32
- # Fetch the main.go file if present, as this will later identify
33
- # this repo as an app.
34
- fetched_files << main if main
35
-
36
- fetched_files
37
45
  end
38
46
 
39
47
  def go_mod
@@ -45,15 +53,21 @@ module Dependabot
45
53
  end
46
54
 
47
55
  def main
48
- return @main if @main
56
+ return @main if defined?(@main)
49
57
 
50
- go_files = repo_contents.select { |f| f.name.end_with?(".go") }
58
+ go_files = Dir.glob("*.go")
51
59
 
52
- go_files.each do |go_file|
53
- file = fetch_file_from_host(go_file.name, type: "package_main")
54
- next unless file.content.match?(/\s*package\s+main/)
60
+ go_files.each do |filename|
61
+ file_content = File.read(filename)
62
+ next unless file_content.match?(/\s*package\s+main/)
55
63
 
56
- return @main = file.tap { |f| f.support_file = true }
64
+ return @main = DependencyFile.new(
65
+ name: Pathname.new(filename).cleanpath.to_path,
66
+ directory: "/",
67
+ type: "package_main",
68
+ support_file: true,
69
+ content: file_content
70
+ )
57
71
  end
58
72
 
59
73
  nil
@@ -3,12 +3,34 @@
3
3
  require "dependabot/shared_helpers"
4
4
  require "dependabot/file_updaters"
5
5
  require "dependabot/file_updaters/base"
6
+ require "dependabot/file_updaters/vendor_updater"
6
7
 
7
8
  module Dependabot
8
9
  module GoModules
9
10
  class FileUpdater < Dependabot::FileUpdaters::Base
10
11
  require_relative "file_updater/go_mod_updater"
11
12
 
13
+ def initialize(dependencies:, dependency_files:, repo_contents_path: nil,
14
+ credentials:, options: {})
15
+ super
16
+ return unless repo_contents_path.nil?
17
+
18
+ # masquerade repo_contents_path for GoModUpdater during transition
19
+ tmp = Dir.mktmpdir
20
+ Dir.chdir(tmp) do
21
+ dependency_files.each do |file|
22
+ File.write(file.name, file.content)
23
+ end
24
+ `git config --global user.email "no-reply@github.com"`
25
+ `git config --global user.name "Dependabot"`
26
+ `git init .`
27
+ `git add .`
28
+ `git commit -m'fake repo_contents_path'`
29
+ end
30
+ @repo_contents_path = tmp
31
+ @repo_contents_stub = true
32
+ end
33
+
12
34
  def self.updated_files_regex
13
35
  [
14
36
  /^go\.mod$/,
@@ -33,6 +55,12 @@ module Dependabot
33
55
  content: file_updater.updated_go_sum_content
34
56
  )
35
57
  end
58
+
59
+ vendor_updater.
60
+ updated_vendor_cache_files(base_directory: directory).
61
+ each do |file|
62
+ updated_files << file
63
+ end
36
64
  end
37
65
 
38
66
  raise "No files changed!" if updated_files.none?
@@ -56,15 +84,40 @@ module Dependabot
56
84
  @go_sum ||= get_original_file("go.sum")
57
85
  end
58
86
 
87
+ def directory
88
+ dependency_files.first.directory
89
+ end
90
+
91
+ def vendor_dir
92
+ File.join(repo_contents_path, directory, "vendor")
93
+ end
94
+
95
+ def vendor_updater
96
+ Dependabot::FileUpdaters::VendorUpdater.new(
97
+ repo_contents_path: repo_contents_path,
98
+ vendor_dir: vendor_dir
99
+ )
100
+ end
101
+
59
102
  def file_updater
60
103
  @file_updater ||=
61
104
  GoModUpdater.new(
62
105
  dependencies: dependencies,
63
- go_mod: go_mod,
64
- go_sum: go_sum,
65
- credentials: credentials
106
+ credentials: credentials,
107
+ repo_contents_path: repo_contents_path,
108
+ directory: directory,
109
+ options: { tidy: tidy?, vendor: vendor? }
66
110
  )
67
111
  end
112
+
113
+ def tidy?
114
+ !@repo_contents_stub && options.fetch(:go_mod_tidy, false)
115
+ end
116
+
117
+ def vendor?
118
+ File.exist?(File.join(vendor_dir, "modules.txt")) &&
119
+ options.fetch(:go_mod_vendor, false)
120
+ end
68
121
  end
69
122
  end
70
123
  end
@@ -25,11 +25,14 @@ module Dependabot
25
25
  /go: ([^@\s]+)(?:@[^\s]+)?: .* declares its path as: ([\S]*)/m
26
26
  ].freeze
27
27
 
28
- def initialize(dependencies:, go_mod:, go_sum:, credentials:)
28
+ def initialize(dependencies:, credentials:, repo_contents_path:,
29
+ directory:, options:)
29
30
  @dependencies = dependencies
30
- @go_mod = go_mod
31
- @go_sum = go_sum
32
31
  @credentials = credentials
32
+ @repo_contents_path = repo_contents_path
33
+ @directory = directory
34
+ @tidy = options.fetch(:tidy, false)
35
+ @vendor = options.fetch(:vendor, false)
33
36
  end
34
37
 
35
38
  def updated_go_mod_content
@@ -42,64 +45,83 @@ module Dependabot
42
45
 
43
46
  private
44
47
 
45
- attr_reader :dependencies, :go_mod, :go_sum, :credentials
48
+ attr_reader :dependencies, :credentials, :repo_contents_path,
49
+ :directory
46
50
 
47
51
  def updated_files
48
52
  @updated_files ||= update_files
49
53
  end
50
54
 
51
- # rubocop:disable Metrics/AbcSize
52
- def update_files
53
- # Map paths in local replace directives to path hashes
54
- substitutions = replace_directive_substitutions(go_mod.content)
55
- stub_dirs = substitutions.values
55
+ def update_files # rubocop:disable Metrics/AbcSize
56
+ in_repo_path do
57
+ # Map paths in local replace directives to path hashes
56
58
 
57
- # Replace full paths with path hashes in the go.mod
58
- clean_go_mod = substitute_all(go_mod.content, substitutions)
59
+ original_go_mod = File.read("go.mod")
60
+ original_manifest = parse_manifest
61
+ original_go_sum = File.read("go.sum") if File.exist?("go.sum")
59
62
 
60
- # Set the new dependency versions in the go.mod
61
- updated_go_mod = in_temp_dir(stub_dirs) do
62
- update_go_mod(clean_go_mod, dependencies)
63
- end
63
+ substitutions = replace_directive_substitutions(original_manifest)
64
+ build_module_stubs(substitutions.values)
64
65
 
65
- # Then run `go get` to pick up other changes to the file caused by
66
- # the upgrade
67
- regenerated_files = in_temp_dir(stub_dirs) do
68
- run_go_get(updated_go_mod, go_sum)
69
- end
66
+ # Replace full paths with path hashes in the go.mod
67
+ substitute_all(substitutions)
70
68
 
71
- # At this point, the go.mod returned from run_go_get contains the
72
- # correct set of modules, but running `go get` can change the file in
73
- # undesirable ways (such as injecting the current Go version), so we
74
- # need to update the original go.mod with the updated set of
75
- # requirements rather than using the regenerated file directly
76
- original_reqs = in_temp_dir(stub_dirs) do
77
- parse_manifest_requirements(go_mod.content)
78
- end
79
- updated_reqs = in_temp_dir(stub_dirs) do
80
- parse_manifest_requirements(regenerated_files[:go_mod])
81
- end
69
+ # Set the stubbed replace directives
70
+ update_go_mod(dependencies)
82
71
 
83
- original_paths = original_reqs.map { |r| r["Path"] }
84
- updated_paths = updated_reqs.map { |r| r["Path"] }
85
- req_paths_to_remove = original_paths - updated_paths
72
+ # Then run `go get` to pick up other changes to the file caused by
73
+ # the upgrade
74
+ run_go_get
75
+ run_go_vendor
76
+ run_go_mod_tidy
86
77
 
87
- output_go_mod = in_temp_dir(stub_dirs) do
88
- remove_requirements(go_mod.content, req_paths_to_remove)
89
- end
78
+ # At this point, the go.mod returned from run_go_get contains the
79
+ # correct set of modules, but running `go get` can change the file
80
+ # in undesirable ways (such as injecting the current Go version),
81
+ # so we need to update the original go.mod with the updated set of
82
+ # requirements rather than using the regenerated file directly
83
+ original_reqs = original_manifest["Require"] || []
84
+ updated_reqs = parse_manifest["Require"] || []
90
85
 
91
- output_go_mod = in_temp_dir(stub_dirs) do
86
+ original_paths = original_reqs.map { |r| r["Path"] }
87
+ updated_paths = updated_reqs.map { |r| r["Path"] }
88
+ req_paths_to_remove = original_paths - updated_paths
89
+
90
+ # Put back the original content before we replace just the updated
91
+ # dependencies.
92
+ write_go_mod(original_go_mod)
93
+
94
+ remove_requirements(req_paths_to_remove)
92
95
  deps = updated_reqs.map { |r| requirement_to_dependency_obj(r) }
93
- update_go_mod(output_go_mod, deps)
96
+ update_go_mod(deps)
97
+
98
+ # put the old replace directives back again
99
+ substitute_all(substitutions.invert)
100
+
101
+ updated_go_sum = original_go_sum ? File.read("go.sum") : nil
102
+ updated_go_mod = File.read("go.mod")
103
+
104
+ { go_mod: updated_go_mod, go_sum: updated_go_sum }
94
105
  end
106
+ end
107
+
108
+ def run_go_mod_tidy
109
+ return unless tidy?
95
110
 
96
- { go_mod: output_go_mod, go_sum: regenerated_files[:go_sum] }
111
+ command = "go mod tidy"
112
+ _, stderr, status = Open3.capture3(ENVIRONMENT, command)
113
+ handle_subprocess_error(stderr) unless status.success?
97
114
  end
98
- # rubocop:enable Metrics/AbcSize
99
115
 
100
- def update_go_mod(go_mod_content, dependencies)
101
- File.write("go.mod", go_mod_content)
116
+ def run_go_vendor
117
+ return unless vendor?
118
+
119
+ command = "go mod vendor"
120
+ _, stderr, status = Open3.capture3(ENVIRONMENT, command)
121
+ handle_subprocess_error(stderr) unless status.success?
122
+ end
102
123
 
124
+ def update_go_mod(dependencies)
103
125
  deps = dependencies.map do |dep|
104
126
  {
105
127
  name: dep.name,
@@ -108,78 +130,75 @@ module Dependabot
108
130
  }
109
131
  end
110
132
 
111
- SharedHelpers.run_helper_subprocess(
133
+ body = SharedHelpers.run_helper_subprocess(
112
134
  command: NativeHelpers.helper_path,
113
135
  env: ENVIRONMENT,
114
136
  function: "updateDependencyFile",
115
137
  args: { dependencies: deps }
116
138
  )
139
+
140
+ write_go_mod(body)
117
141
  end
118
142
 
119
- def run_go_get(go_mod_content, go_sum)
120
- File.write("go.mod", go_mod_content)
121
- File.write("go.sum", go_sum.content) if go_sum
122
- File.write("main.go", dummy_main_go)
143
+ def run_go_get
144
+ tmp_go_file = "#{SecureRandom.hex}.go"
145
+
146
+ unless Dir.glob("*.go").any?
147
+ File.write(tmp_go_file, "package dummypkg\n")
148
+ end
123
149
 
124
150
  _, stderr, status = Open3.capture3(ENVIRONMENT, "go get -d")
125
151
  handle_subprocess_error(stderr) unless status.success?
126
-
127
- updated_go_sum = go_sum ? File.read("go.sum") : nil
128
- { go_mod: File.read("go.mod"), go_sum: updated_go_sum }
152
+ ensure
153
+ File.delete(tmp_go_file) if File.exist?(tmp_go_file)
129
154
  end
130
155
 
131
- def parse_manifest_requirements(go_mod_content)
132
- File.write("go.mod", go_mod_content)
133
-
156
+ def parse_manifest
134
157
  command = "go mod edit -json"
135
158
  stdout, stderr, status = Open3.capture3(ENVIRONMENT, command)
136
159
  handle_subprocess_error(stderr) unless status.success?
137
160
 
138
- JSON.parse(stdout)["Require"] || []
161
+ JSON.parse(stdout) || {}
139
162
  end
140
163
 
141
- def remove_requirements(go_mod_content, requirement_paths)
142
- File.write("go.mod", go_mod_content)
143
-
164
+ def remove_requirements(requirement_paths)
144
165
  requirement_paths.each do |path|
145
166
  escaped_path = Shellwords.escape(path)
146
167
  command = "go mod edit -droprequire #{escaped_path}"
147
168
  _, stderr, status = Open3.capture3(ENVIRONMENT, command)
148
169
  handle_subprocess_error(stderr) unless status.success?
149
170
  end
150
-
151
- File.read("go.mod")
152
171
  end
153
172
 
154
- def add_requirements(go_mod_content, requirements)
155
- File.write("go.mod", go_mod_content)
156
-
173
+ def add_requirements(requirements)
157
174
  requirements.each do |r|
158
175
  escaped_req = Shellwords.escape("#{r['Path']}@#{r['Version']}")
159
176
  command = "go mod edit -require #{escaped_req}"
160
177
  _, stderr, status = Open3.capture3(ENVIRONMENT, command)
161
178
  handle_subprocess_error(stderr) unless status.success?
162
179
  end
163
-
164
- File.read("go.mod")
165
180
  end
166
181
 
167
- def in_temp_dir(stub_paths, &block)
168
- SharedHelpers.in_a_temporary_directory do
182
+ def in_repo_path(&block)
183
+ SharedHelpers.
184
+ in_a_temporary_repo_directory(directory, repo_contents_path) do
169
185
  SharedHelpers.with_git_configured(credentials: credentials) do
170
- # Create a fake empty module for each local module so that
171
- # `go get -d` works, even if some modules have been `replace`d
172
- # with a local module that we don't have access to.
173
- stub_paths.each do |stub_path|
174
- Dir.mkdir(stub_path) unless Dir.exist?(stub_path)
175
- FileUtils.touch(File.join(stub_path, "go.mod"))
176
- end
177
-
178
186
  block.call
179
187
  end
180
188
  end
181
189
  end
182
190
 
191
+ def build_module_stubs(stub_paths)
192
+ # Create a fake empty module for each local module so that
193
+ # `go get -d` works, even if some modules have been `replace`d
194
+ # with a local module that we don't have access to.
195
+ stub_paths.each do |stub_path|
196
+ Dir.mkdir(stub_path) unless Dir.exist?(stub_path)
197
+ FileUtils.touch(File.join(stub_path, "go.mod"))
198
+ FileUtils.touch(File.join(stub_path, "main.go"))
199
+ end
200
+ end
201
+
183
202
  # Given a go.mod file, find all `replace` directives pointing to a path
184
203
  # on the local filesystem, and return an array of pairs mapping the
185
204
  # original path to a hash of the path.
@@ -188,22 +207,14 @@ module Dependabot
188
207
  # the layout of the filesystem with a structure we can reproduce (i.e.
189
208
  # no paths such as ../../../foo), run the Go tooling, then reverse the
190
209
  # process afterwards.
191
- def replace_directive_substitutions(go_mod_content)
210
+ def replace_directive_substitutions(manifest)
192
211
  @replace_directive_substitutions ||=
193
- SharedHelpers.in_a_temporary_directory do |path|
194
- File.write("go.mod", go_mod_content)
195
-
196
- # Parse the go.mod to get a JSON representation of the replace
197
- # directives
198
- command = "go mod edit -json"
199
- stdout, stderr, status = Open3.capture3(ENVIRONMENT, command)
200
- handle_subprocess_error(path, stderr) unless status.success?
201
-
212
+ begin
202
213
  # Find all the local replacements, and return them with a stub
203
214
  # path we can use in their place. Using generated paths is safer
204
215
  # as it means we don't need to worry about references to parent
205
216
  # directories, etc.
206
- (JSON.parse(stdout)["Replace"] || []).
217
+ (manifest["Replace"] || []).
207
218
  map { |r| r["New"]["Path"] }.
208
219
  compact.
209
220
  select { |p| p.start_with?(".") || p.start_with?("/") }.
@@ -212,10 +223,12 @@ module Dependabot
212
223
  end
213
224
  end
214
225
 
215
- def substitute_all(file, substitutions)
216
- substitutions.reduce(file) do |text, (a, b)|
226
+ def substitute_all(substitutions)
227
+ body = substitutions.reduce(File.read("go.mod")) do |text, (a, b)|
217
228
  text.sub(a, b)
218
229
  end
230
+
231
+ write_go_mod(body)
219
232
  end
220
233
 
221
234
  def handle_subprocess_error(stderr)
@@ -231,28 +244,18 @@ module Dependabot
231
244
  if path_regex
232
245
  match = path_regex.match(stderr)
233
246
  raise Dependabot::GoModulePathMismatch.
234
- new(go_mod.path, match[1], match[2])
247
+ new(go_mod_path, match[1], match[2])
235
248
  end
236
249
 
237
250
  msg = stderr.lines.last(10).join.strip
238
- raise Dependabot::DependencyFileNotParseable.new(go_mod.path, msg)
251
+ raise Dependabot::DependencyFileNotParseable.
252
+ new(go_mod_path, msg)
239
253
  end
240
254
 
241
- def dummy_main_go
242
- # If we use `main` as the package name, running `go get -d` seems to
243
- # invoke the build systems, which can cause problems. For instance,
244
- # if the go.mod includes a module that doesn't have a top-level
245
- # package, we have no way of working out the import path, so the
246
- # build step fails.
247
- #
248
- # In due course, if we end up fetching the full repo, it might be
249
- # good to switch back to `main` so we can surface more errors.
250
- lines = ["package dummypkg", "import ("]
251
- dependencies.each do |dep|
252
- lines << "_ \"#{dep.name}\"" unless dep.requirements.empty?
253
- end
254
- lines << ")"
255
- lines.join("\n")
255
+ def go_mod_path
256
+ return "go.mod" if directory == "/"
257
+
258
+ File.join(directory, "go.mod")
256
259
  end
257
260
 
258
261
  def requirement_to_dependency_obj(req)
@@ -272,6 +275,18 @@ module Dependabot
272
275
  package_manager: "go_modules"
273
276
  )
274
277
  end
278
+
279
+ def write_go_mod(body)
280
+ File.write("go.mod", body)
281
+ end
282
+
283
+ def tidy?
284
+ !!@tidy
285
+ end
286
+
287
+ def vendor?
288
+ !!@vendor
289
+ end
275
290
  end
276
291
  end
277
292
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-go_modules
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.120.3
4
+ version: 0.122.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-28 00:00:00.000000000 Z
11
+ date: 2020-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.120.3
19
+ version: 0.122.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.120.3
26
+ version: 0.122.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: byebug
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -100,14 +100,42 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 0.91.0
103
+ version: 0.92.0
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 0.91.0
110
+ version: 0.92.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.19.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.19.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov-console
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.7.2
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.7.2
111
139
  - !ruby/object:Gem::Dependency
112
140
  name: vcr
113
141
  requirement: !ruby/object:Gem::Requirement