fpm 1.8.1 → 1.9.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -44,9 +44,27 @@ class FPM::Package::Gem < FPM::Package
44
44
  option "--disable-dependency", "gem_name",
45
45
  "The gem name to remove from dependency list",
46
46
  :multivalued => true, :attribute_name => :gem_disable_dependencies
47
+ option "--embed-dependencies", :flag, "Should the gem dependencies " \
48
+ "be installed?", :default => false
47
49
 
48
50
  option "--version-bins", :flag, "Append the version to the bins", :default => false
49
51
 
52
+ option "--stagingdir", "STAGINGDIR",
53
+ "The directory where fpm installs the gem temporarily before conversion. " \
54
+ "Normally a random subdirectory of workdir."
55
+
56
+ # Override parent method
57
+ def staging_path(path=nil)
58
+ @gem_staging_path ||= attributes[:gem_stagingdir] || Stud::Temporary.directory("package-#{type}-staging")
59
+ @staging_path = @gem_staging_path
60
+
61
+ if path.nil?
62
+ return @staging_path
63
+ else
64
+ return File.join(@staging_path, path)
65
+ end
66
+ end # def staging_path
67
+
50
68
  def input(gem)
51
69
  # 'arg' is the name of the rubygem we should unpack.
52
70
  path_to_gem = download_if_necessary(gem, version)
@@ -144,7 +162,7 @@ class FPM::Package::Gem < FPM::Package
144
162
  # composing multiple packages, it's best to explicitly include it in the provides list.
145
163
  self.provides << "#{self.name} = #{self.version}"
146
164
 
147
- if !attributes[:no_auto_depends?]
165
+ if !attributes[:no_auto_depends?] && !attributes[:gem_embed_dependencies?]
148
166
  spec.runtime_dependencies.map do |dep|
149
167
  # rubygems 1.3.5 doesn't have 'Gem::Dependency#requirement'
150
168
  if dep.respond_to?(:requirement)
@@ -181,7 +199,12 @@ class FPM::Package::Gem < FPM::Package
181
199
  ::FileUtils.mkdir_p(installdir)
182
200
  # TODO(sissel): Allow setting gem tool path
183
201
  args = [attributes[:gem_gem], "install", "--quiet", "--no-ri", "--no-rdoc",
184
- "--no-user-install", "--install-dir", installdir, "--ignore-dependencies"]
202
+ "--no-user-install", "--install-dir", installdir]
203
+
204
+ if !attributes[:gem_embed_dependencies?]
205
+ args += ["--ignore-dependencies"]
206
+ end
207
+
185
208
  if attributes[:gem_env_shebang?]
186
209
  args += ["-E"]
187
210
  end
@@ -231,6 +254,21 @@ class FPM::Package::Gem < FPM::Package
231
254
  FileUtils.mv("#{bin_path}/#{bin}", "#{bin_path}/#{bin}-#{self.version}")
232
255
  end
233
256
  end
257
+
258
+ if attributes[:source_date_epoch_from_changelog?]
259
+ detect_source_date_from_changelog(installdir)
260
+ end
261
+
262
+ # Remove generated Makefile and gem_make.out files, if any; they
263
+ # are not needed, and may contain generated paths that cause
264
+ # different output on successive runs.
265
+ Find.find(installdir) do |path|
266
+ if path =~ /.*(gem_make.out|Makefile|mkmf.log)$/
267
+ logger.info("Removing no longer needed file %s to reduce nondeterminism" % path)
268
+ File.unlink(path)
269
+ end
270
+ end
271
+
234
272
  end # def install_to_staging
235
273
 
236
274
  # Sanitize package name.
@@ -239,5 +277,75 @@ class FPM::Package::Gem < FPM::Package
239
277
  def fix_name(name)
240
278
  return [attributes[:gem_package_name_prefix], name].join("-")
241
279
  end # def fix_name
280
+
281
+ # Regular expression to accept a gem changelog line, and store date & version, if any, in named capture groups.
282
+ # Supports formats suggested by http://keepachangelog.com and https://github.com/tech-angels/vandamme
283
+ # as well as other similar formats that actually occur in the wild.
284
+ # Build it in pieces for readability, and allow version and date in either order.
285
+ # Whenever you change this, add a row to the test case in spec/fpm/package/gem_spec.rb.
286
+ # Don't even try to handle dates that lack four-digit years.
287
+ # Building blocks:
288
+ P_RE_LEADIN = '^[#=]{0,3}\s?'
289
+ P_RE_VERSION_ = '[\w\.-]+\.[\w\.-]+[a-zA-Z0-9]'
290
+ P_RE_SEPARATOR = '\s[-=/(]?\s?'
291
+ P_RE_DATE1 = '\d{4}-\d{2}-\d{2}'
292
+ P_RE_DATE2 = '\w+ \d{1,2}(?:st|nd|rd|th)?,\s\d{4}'
293
+ P_RE_DATE3 = '\w+\s+\w+\s+\d{1,2},\s\d{4}'
294
+ P_RE_DATE = "(?<date>#{P_RE_DATE1}|#{P_RE_DATE2}|#{P_RE_DATE3})"
295
+ P_RE_URL = '\(https?:[-\w/.%]*\)' # In parens, per markdown
296
+ P_RE_GTMAGIC = '\[\]' # github magic version diff, per chandler
297
+ P_RE_VERSION = "\\[?(?:Version |v)?(?<version>#{P_RE_VERSION_})\\]?(?:#{P_RE_URL}|#{P_RE_GTMAGIC})?"
298
+ # The final RE's:
299
+ P_RE_VERSION_DATE = "#{P_RE_LEADIN}#{P_RE_VERSION}#{P_RE_SEPARATOR}#{P_RE_DATE}"
300
+ P_RE_DATE_VERSION = "#{P_RE_LEADIN}#{P_RE_DATE}#{P_RE_SEPARATOR}#{P_RE_VERSION}"
301
+
302
+ # Detect release date, if found, store in attributes[:source_date_epoch]
303
+ def detect_source_date_from_changelog(installdir)
304
+ name = self.name.sub("rubygem-", "") + "-" + self.version
305
+ changelog = nil
306
+ datestr = nil
307
+ r1 = Regexp.new(P_RE_VERSION_DATE)
308
+ r2 = Regexp.new(P_RE_DATE_VERSION)
309
+
310
+ # Changelog doesn't have a standard name, so check all common variations
311
+ # Sort this list using LANG=C, i.e. caps first
312
+ [
313
+ "CHANGELIST",
314
+ "CHANGELOG", "CHANGELOG.asciidoc", "CHANGELOG.md", "CHANGELOG.rdoc", "CHANGELOG.rst", "CHANGELOG.txt",
315
+ "CHANGES", "CHANGES.md", "CHANGES.txt",
316
+ "ChangeLog", "ChangeLog.md", "ChangeLog.txt",
317
+ "Changelog", "Changelog.md", "Changelog.txt",
318
+ "changelog", "changelog.md", "changelog.txt",
319
+ ].each do |changelogname|
320
+ path = File.join(installdir, "gems", name, changelogname)
321
+ if File.exist?(path)
322
+ changelog = path
323
+ File.open path do |file|
324
+ file.each_line do |line|
325
+ if line =~ /#{self.version}/
326
+ [r1, r2].each do |r|
327
+ if r.match(line)
328
+ datestr = $~[:date]
329
+ break
330
+ end
331
+ end
332
+ end
333
+ end
334
+ end
335
+ end
336
+ end
337
+ if datestr
338
+ date = Date.parse(datestr)
339
+ sec = date.strftime("%s")
340
+ attributes[:source_date_epoch] = sec
341
+ logger.debug("Gem %s has changelog date %s, setting source_date_epoch to %s" % [name, datestr, sec])
342
+ elsif changelog
343
+ logger.debug("Gem %s changelog %s did not have recognizable date for release %s" % [name, changelog, self.version])
344
+ else
345
+ logger.debug("Gem %s did not have changelog with recognized name" % [name])
346
+ # FIXME: check rubygems.org?
347
+ end
348
+ end # detect_source_date_from_changelog
349
+
242
350
  public(:input, :output)
243
351
  end # class FPM::Package::Gem
@@ -10,7 +10,7 @@ require "pleaserun/cli"
10
10
  # This does not currently support 'output'
11
11
  class FPM::Package::PleaseRun < FPM::Package
12
12
  # TODO(sissel): Implement flags.
13
-
13
+
14
14
  require "pleaserun/platform/systemd"
15
15
  require "pleaserun/platform/upstart"
16
16
  require "pleaserun/platform/launchd"
@@ -28,6 +28,10 @@ class FPM::Package::PleaseRun < FPM::Package
28
28
  ::PleaseRun::Platform::Launchd.new("10.9"), # OS X
29
29
  ::PleaseRun::Platform::SYSV.new("lsb-3.1") # Ancient stuff
30
30
  ]
31
+ pleaserun_attributes = [ "chdir", "user", "group", "umask", "chroot", "nice", "limit_coredump",
32
+ "limit_cputime", "limit_data", "limit_file_size", "limit_locked_memory",
33
+ "limit_open_files", "limit_user_processes", "limit_physical_memory", "limit_stack_size",
34
+ "log_directory", "log_file_stderr", "log_file_stdout"]
31
35
 
32
36
  attributes[:pleaserun_name] ||= File.basename(command.first)
33
37
  attributes[:prefix] ||= "/usr/share/pleaserun/#{attributes[:pleaserun_name]}"
@@ -37,12 +41,18 @@ class FPM::Package::PleaseRun < FPM::Package
37
41
  platform.program = command.first
38
42
  platform.name = attributes[:pleaserun_name]
39
43
  platform.args = command[1..-1]
40
- platform.chdir = attributes[:pleaserun_chdir] if attributes[:pleaserun_chdir]
41
44
  platform.description = if attributes[:description_given?]
42
45
  attributes[:description]
43
46
  else
44
47
  platform.name
45
48
  end
49
+ pleaserun_attributes.each do |attribute_name|
50
+ attribute = "pleaserun_#{attribute_name}".to_sym
51
+ if attributes.has_key?(attribute) and not attributes[attribute].nil?
52
+ platform.send("#{attribute_name}=", attributes[attribute])
53
+ end
54
+ end
55
+
46
56
  base = staging_path(File.join(attributes[:prefix], "#{platform.platform}/#{platform.target_version || "default"}"))
47
57
  target = File.join(base, "files")
48
58
  actions_script = File.join(base, "install_actions.sh")
@@ -73,6 +73,10 @@ class FPM::Package::Python < FPM::Package
73
73
  "The python package name to remove from dependency list",
74
74
  :multivalued => true, :attribute_name => :python_disable_dependency,
75
75
  :default => []
76
+ option "--setup-py-arguments", "setup_py_argument",
77
+ "Arbitrary argument(s) to be passed to setup.py",
78
+ :multivalued => true, :attribute_name => :python_setup_py_arguments,
79
+ :default => []
76
80
 
77
81
  private
78
82
 
@@ -310,6 +314,13 @@ class FPM::Package::Python < FPM::Package
310
314
  flags += [ "build_scripts", "--executable", attributes[:python_scripts_executable] ]
311
315
  end
312
316
 
317
+ if !attributes[:python_setup_py_arguments].nil? and !attributes[:python_setup_py_arguments].empty?
318
+ # Add optional setup.py arguments
319
+ attributes[:python_setup_py_arguments].each do |a|
320
+ flags += [ a ]
321
+ end
322
+ end
323
+
313
324
  safesystem(attributes[:python_bin], "setup.py", "install", *flags)
314
325
  end
315
326
  end # def install_to_staging
@@ -472,6 +472,15 @@ class FPM::Package::RPM < FPM::Package
472
472
  self.directories = alldirs
473
473
  end
474
474
 
475
+ # include external config files
476
+ (attributes[:config_files] or []).each do |conf|
477
+ path = conf
478
+ dest_conf = File.join(staging_path, path)
479
+ FileUtils.mkdir_p(File.dirname(dest_conf))
480
+ FileUtils.cp_r conf, dest_conf
481
+ File.chmod(0755, dest_conf)
482
+ end
483
+
475
484
  # scan all conf file paths for files and add them
476
485
  allconfigs = []
477
486
  self.config_files.each do |path|
@@ -50,15 +50,7 @@ class FPM::Package::Tar < FPM::Package
50
50
  output_check(output_path)
51
51
 
52
52
  # Write the scripts, too.
53
- scripts_path = File.join(staging_path, ".scripts")
54
- ::Dir.mkdir(scripts_path)
55
- [:before_install, :after_install, :before_remove, :after_remove].each do |name|
56
- next unless script?(name)
57
- out = File.join(scripts_path, name.to_s)
58
- logger.debug("Writing script", :source => name, :target => out)
59
- File.write(out, script(name))
60
- File.chmod(0755, out)
61
- end
53
+ write_scripts
62
54
 
63
55
  # Unpack the tarball to the staging path
64
56
  args = ["-cf", output_path, "-C", staging_path]
@@ -15,8 +15,11 @@ class FPM::Package::Virtualenv < FPM::Package
15
15
  option "--package-name-prefix", "PREFIX", "Name to prefix the package " \
16
16
  "name with.", :default => "virtualenv"
17
17
 
18
- option "--install-location", "DIRECTORY", "Location to which to " \
19
- "install the virtualenv by default.", :default => "/usr/share/python" do |path|
18
+ option "--install-location", "DIRECTORY", "DEPRECATED: Use --prefix instead." \
19
+ " Location to which to install the virtualenv by default.",
20
+ :default => "/usr/share/python" do |path|
21
+ logger.warn("Using deprecated flag: --install-location. Please use " \
22
+ "--prefix instead.")
20
23
  File.expand_path(path)
21
24
  end
22
25
 
@@ -37,6 +40,12 @@ class FPM::Package::Virtualenv < FPM::Package
37
40
  option "--system-site-packages", :flag, "Give the virtual environment access to the "\
38
41
  "global site-packages"
39
42
 
43
+ option "--find-links", "PIP_FIND_LINKS", "If a url or path to an html file, then parse for "\
44
+ "links to archives. If a local path or file:// url that's a directory, then look "\
45
+ "for archives in the directory listing.",
46
+ :multivalued => true, :attribute_name => :virtualenv_find_links_urls,
47
+ :default => nil
48
+
40
49
  private
41
50
 
42
51
  # Input a package.
@@ -76,9 +85,14 @@ class FPM::Package::Virtualenv < FPM::Package
76
85
  self.name].join("-")
77
86
  end
78
87
 
88
+ # prefix wins over previous virtual_install_location behaviour
79
89
  virtualenv_folder =
80
- File.join(installdir,
81
- virtualenv_name)
90
+ if self.attributes[:prefix]
91
+ self.attributes[:prefix]
92
+ else
93
+ File.join(installdir,
94
+ virtualenv_name)
95
+ end
82
96
 
83
97
  virtualenv_build_folder = build_path(virtualenv_folder)
84
98
 
@@ -107,6 +121,13 @@ class FPM::Package::Virtualenv < FPM::Package
107
121
  end
108
122
  end
109
123
 
124
+ find_links_url_args = []
125
+ if attributes[:virtualenv_find_links_urls]
126
+ attributes[:virtualenv_find_links_urls].each do |links_url|
127
+ find_links_url_args << "--find-links" << links_url
128
+ end
129
+ end
130
+
110
131
  target_args = []
111
132
  if is_requirements_file
112
133
  target_args << "-r" << package
@@ -114,7 +135,7 @@ class FPM::Package::Virtualenv < FPM::Package
114
135
  target_args << package
115
136
  end
116
137
 
117
- pip_args = [python_exe, pip_exe, "install", "-i", attributes[:virtualenv_pypi]] << extra_index_url_args << target_args
138
+ pip_args = [python_exe, pip_exe, "install", "-i", attributes[:virtualenv_pypi]] << extra_index_url_args << find_links_url_args << target_args
118
139
  safesystem(*pip_args.flatten)
119
140
 
120
141
  if attributes[:virtualenv_setup_install?]
@@ -155,7 +176,8 @@ class FPM::Package::Virtualenv < FPM::Package
155
176
  # use dir to set stuff up properly, mainly so I don't have to reimplement
156
177
  # the chdir/prefix stuff special for tar.
157
178
  dir = convert(FPM::Package::Dir)
158
-
179
+ # don't double prefix the files
180
+ dir.attributes[:prefix] = nil
159
181
  if attributes[:chdir]
160
182
  dir.attributes[:chdir] = File.join(build_path, attributes[:chdir])
161
183
  else
@@ -36,28 +36,12 @@ class FPM::Package::Zip < FPM::Package
36
36
  dir.cleanup_build
37
37
  end # def input
38
38
 
39
- # Output a tarball.
40
- #
41
- # If the output path ends predictably (like in .tar.gz) it will try to obey
42
- # the compression type.
39
+ # Output a zipfile.
43
40
  def output(output_path)
44
41
  output_check(output_path)
45
-
46
- files = Find.find(staging_path).to_a
47
- safesystem("zip", output_path, *files)
48
- end # def output
49
-
50
- # Generate the proper tar flags based on the path name.
51
- def tar_compression_flag(path)
52
- case path
53
- when /\.tar\.bz2$/
54
- return "-j"
55
- when /\.tar\.gz$|\.tgz$/
56
- return "-z"
57
- when /\.tar\.xz$/
58
- return "-J"
59
- else
60
- return nil
42
+ realpath = Pathname.new(output_path).realdirpath.to_s
43
+ ::Dir.chdir(staging_path) do
44
+ safesystem("zip", "-9r", realpath, ".")
61
45
  end
62
- end # def tar_compression_flag
46
+ end # def output
63
47
  end # class FPM::Package::Tar
@@ -1,6 +1,7 @@
1
1
  require "fpm/namespace"
2
2
  require "childprocess"
3
3
  require "ffi"
4
+ require "fileutils"
4
5
 
5
6
  # Some utility functions
6
7
  module FPM::Util
@@ -190,9 +191,14 @@ module FPM::Util
190
191
  if args.size == 1
191
192
  args = [ default_shell, "-c", args[0] ]
192
193
  end
193
- program = args[0]
194
194
 
195
- exit_code = execmd(args)
195
+ if args[0].kind_of?(Hash)
196
+ env = args.shift()
197
+ exit_code = execmd(env, args)
198
+ else
199
+ exit_code = execmd(args)
200
+ end
201
+ program = args[0]
196
202
  success = (exit_code == 0)
197
203
 
198
204
  if !success
@@ -226,26 +232,90 @@ module FPM::Util
226
232
  return stdout_r_str
227
233
  end # def safesystemout
228
234
 
235
+ # Get an array containing the recommended 'ar' command for this platform
236
+ # and the recommended options to quickly create/append to an archive
237
+ # without timestamps or uids (if possible).
238
+ def ar_cmd
239
+ return @@ar_cmd if defined? @@ar_cmd
240
+
241
+ @@ar_cmd_deterministic = false
242
+
243
+ # FIXME: don't assume current directory writeable
244
+ FileUtils.touch(["fpm-dummy.tmp"])
245
+ ["ar", "gar"].each do |ar|
246
+ ["-qc", "-qcD"].each do |ar_create_opts|
247
+ FileUtils.rm_f(["fpm-dummy.ar.tmp"])
248
+ # Return this combination if it creates archives without uids or timestamps.
249
+ # Exitstatus will be nonzero if the archive can't be created,
250
+ # or its table of contents doesn't match the regular expression.
251
+ # Be extra-careful about locale and timezone when matching output.
252
+ system("#{ar} #{ar_create_opts} fpm-dummy.ar.tmp fpm-dummy.tmp 2>/dev/null && env TZ=UTC LANG=C LC_TIME=C #{ar} -tv fpm-dummy.ar.tmp | grep '0/0.*1970' > /dev/null 2>&1")
253
+ if $?.exitstatus == 0
254
+ @@ar_cmd = [ar, ar_create_opts]
255
+ @@ar_cmd_deterministic = true
256
+ return @@ar_cmd
257
+ end
258
+ end
259
+ end
260
+ # If no combination of ar and options omits timestamps, fall back to default.
261
+ @@ar_cmd = ["ar", "-qc"]
262
+ return @@ar_cmd
263
+ ensure
264
+ # Clean up
265
+ FileUtils.rm_f(["fpm-dummy.ar.tmp", "fpm-dummy.tmp"])
266
+ end # def ar_cmd
267
+
268
+ # Return whether the command returned by ar_cmd can create deterministic archives
269
+ def ar_cmd_deterministic?
270
+ ar_cmd if not defined? @@ar_cmd_deterministic
271
+ return @@ar_cmd_deterministic
272
+ end
273
+
229
274
  # Get the recommended 'tar' command for this platform.
230
275
  def tar_cmd
231
- # Rely on gnu tar for solaris and OSX.
232
- case %x{uname -s}.chomp
233
- when "SunOS"
234
- return "gtar"
235
- when "Darwin"
236
- # Try running gnutar, it was renamed(??) in homebrew to 'gtar' at some point, I guess? I don't know.
237
- ["gnutar", "gtar"].each do |tar|
238
- system("#{tar} > /dev/null 2> /dev/null")
239
- return tar unless $?.exitstatus == 127
276
+ return @@tar_cmd if defined? @@tar_cmd
277
+
278
+ # FIXME: don't assume current directory writeable
279
+ FileUtils.touch(["fpm-dummy.tmp"])
280
+
281
+ # Prefer tar that supports more of the features we want, stop if we find tar of our dreams
282
+ best="tar"
283
+ bestscore=0
284
+ @@tar_cmd_deterministic = false
285
+ # GNU Tar, if not the default, is usually on the path as gtar, but
286
+ # Mac OS X 10.8 and earlier shipped it as /usr/bin/gnutar
287
+ ["tar", "gtar", "gnutar"].each do |tar|
288
+ opts=[]
289
+ score=0
290
+ ["--sort=name", "--mtime=@0"].each do |opt|
291
+ system("#{tar} #{opt} -cf fpm-dummy.tar.tmp fpm-dummy.tmp > /dev/null 2>&1")
292
+ if $?.exitstatus == 0
293
+ opts << opt
294
+ score += 1
295
+ end
296
+ end
297
+ if score > bestscore
298
+ best=tar
299
+ bestscore=score
300
+ if score == 2
301
+ @@tar_cmd_deterministic = true
302
+ break
303
+ end
240
304
  end
241
- when "FreeBSD"
242
- # use gnutar instead
243
- return "gtar"
244
- else
245
- return "tar"
246
305
  end
306
+ @@tar_cmd = best
307
+ return @@tar_cmd
308
+ ensure
309
+ # Clean up
310
+ FileUtils.rm_f(["fpm-dummy.tar.tmp", "fpm-dummy.tmp"])
247
311
  end # def tar_cmd
248
312
 
313
+ # Return whether the command returned by tar_cmd can create deterministic archives
314
+ def tar_cmd_supports_sort_names_and_set_mtime?
315
+ tar_cmd if not defined? @@tar_cmd_deterministic
316
+ return @@tar_cmd_deterministic
317
+ end
318
+
249
319
  # wrapper around mknod ffi calls
250
320
  def mknod_w(path, mode, dev)
251
321
  rc = -1
@@ -298,8 +368,8 @@ module FPM::Util
298
368
  if known_entry
299
369
  FileUtils.ln(known_entry, dst)
300
370
  else
301
- FileUtils.copy_entry(src, dst, preserve=preserve,
302
- remove_destination=remove_destination)
371
+ FileUtils.copy_entry(src, dst, preserve, false,
372
+ remove_destination)
303
373
  copied_entries[[st.dev, st.ino]] = dst
304
374
  end
305
375
  end # else...