fpm 0.4.42 → 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.
@@ -1,4 +1,5 @@
1
1
  require "fpm/package"
2
+ require "fpm/util"
2
3
  require "backports"
3
4
  require "fileutils"
4
5
  require "find"
@@ -42,14 +43,15 @@ class FPM::Package::Dir < FPM::Package
42
43
  # This mapping should work the same way 'rsync -a' does
43
44
  # Meaning 'rsync -a source dest'
44
45
  # and 'source=dest' in fpm work the same as the above rsync
45
- if path =~ /.=./
46
+ if path =~ /.=./ && !File.exists?(chdir == '.' ? path : File.join(chdir, path))
46
47
  origin, destination = path.split("=", 2)
47
48
 
48
49
  if File.directory?(origin) && origin[-1,1] == "/"
49
- chdir = origin
50
+ chdir = chdir == '.' ? origin : File.join(chdir, origin)
50
51
  source = "."
51
52
  else
52
- chdir = File.dirname(origin)
53
+ origin_dir = File.dirname(origin)
54
+ chdir = chdir == '.' ? origin_dir : File.join(chdir, origin_dir)
53
55
  source = File.basename(origin)
54
56
  end
55
57
  else
@@ -63,8 +65,18 @@ class FPM::Package::Dir < FPM::Package
63
65
  destination = File.join(staging_path, destination)
64
66
 
65
67
  @logger["method"] = "input"
66
- ::Dir.chdir(chdir) do
67
- clone(source, destination)
68
+ begin
69
+ ::Dir.chdir(chdir) do
70
+ begin
71
+ clone(source, destination)
72
+ rescue Errno::ENOENT => e
73
+ raise FPM::InvalidPackageConfiguration,
74
+ "Cannot package the path '#{source}', does it exist?"
75
+ end
76
+ end
77
+ rescue Errno::ENOENT => e
78
+ raise FPM::InvalidPackageConfiguration,
79
+ "Cannot chdir to '#{chdir}'. Does it exist?"
68
80
  end
69
81
 
70
82
  # Set some defaults. This is useful because other package types
@@ -105,6 +117,20 @@ class FPM::Package::Dir < FPM::Package
105
117
  # /tmp/example/hello/world
106
118
  def clone(source, destination)
107
119
  @logger.debug("Cloning path", :source => source, :destination => destination)
120
+ # Edge case check; abort if the temporary directory is the source.
121
+ # If the temporary dir is the same path as the source, it causes
122
+ # fpm to recursively (and forever) copy the staging directory by
123
+ # accident (#542).
124
+ if File.expand_path(source) == File.expand_path(::Dir.tmpdir)
125
+ raise FPM::InvalidPackageConfiguration,
126
+ "A source directory cannot be the root of your temporary " \
127
+ "directory (#{::Dir.tmpdir}). fpm uses the temporary directory " \
128
+ "to stage files during packaging, so this setting would have " \
129
+ "caused fpm to loop creating staging directories and copying " \
130
+ "them into your package! Oops! If you are confused, maybe you could " \
131
+ "check your TMPDIR or TEMPDIR environment variables?"
132
+ end
133
+
108
134
  # For single file copies, permit file destinations
109
135
  if File.file?(source) && !File.directory?(destination)
110
136
  if destination[-1,1] == "/"
@@ -155,7 +181,7 @@ class FPM::Package::Dir < FPM::Package
155
181
  rescue Errno::ENOENT, Errno::EXDEV, Errno::EPERM
156
182
  # Hardlink attempt failed, copy it instead
157
183
  @logger.debug("Copying", :source => source, :destination => destination)
158
- FileUtils.copy_entry(source, destination)
184
+ copy_entry(source, destination)
159
185
  rescue Errno::EEXIST
160
186
  sane_path = destination.gsub(staging_path, "")
161
187
  @logger.error("Cannot copy file, the destination path is probably a directory and I attempted to write a file.", :path => sane_path, :staging => staging_path)
@@ -89,7 +89,7 @@ class FPM::Package::Gem < FPM::Package
89
89
 
90
90
  def load_package_info(gem_path)
91
91
 
92
- spec = YAML.load(%x{#{attributes[:gem_gem]} spec #{gem_path} --yaml})
92
+ spec = YAML.load(%x{#{attributes[:gem_gem]} specification #{gem_path} --yaml})
93
93
 
94
94
  if !attributes[:gem_package_prefix].nil?
95
95
  attributes[:gem_package_name_prefix] = attributes[:gem_package_prefix]
@@ -11,6 +11,9 @@ class FPM::Package::NPM < FPM::Package
11
11
  option "--package-name-prefix", "PREFIX", "Name to prefix the package " \
12
12
  "name with.", :default => "node"
13
13
 
14
+ option "--registry", "NPM_REGISTRY",
15
+ "The npm registry to use instead of the default."
16
+
14
17
  private
15
18
  def input(package)
16
19
  # Notes:
@@ -21,6 +24,10 @@ class FPM::Package::NPM < FPM::Package
21
24
  "global" => "true"
22
25
  }
23
26
 
27
+ if attributes.include?(:npm_registry) && !attributes[:npm_registry].nil?
28
+ settings["registry"] = attributes[:npm_registry]
29
+ end
30
+
24
31
  if attributes.include?(:prefix) && !attributes[:prefix].nil?
25
32
  settings["prefix"] = staging_path(attributes[:prefix])
26
33
  else
@@ -0,0 +1,35 @@
1
+ class FPM::Package::Pkgin < FPM::Package
2
+
3
+ def output(output_path)
4
+ output_check(output_path)
5
+
6
+ File.write(build_path("build-info"), `pkg_info -X pkg_install | egrep '^(MACHINE_ARCH|OPSYS|OS_VERSION|PKGTOOLS_VERSION)'`)
7
+
8
+ cwd = ::Dir.pwd
9
+ ::Dir.chdir(staging_path)
10
+
11
+ files = []
12
+ Find.find(".") do |path|
13
+ stat = File.lstat(path)
14
+ next unless stat.symlink? or stat.file?
15
+ files << path
16
+ end
17
+ ::Dir.chdir(cwd)
18
+
19
+ File.write(build_path("packlist"), files.sort.join("\n"))
20
+
21
+ File.write(build_path("comment"), self.description + "\n")
22
+
23
+ File.write(build_path("description"), self.description + "\n")
24
+
25
+ args = [ "-B", build_path("build-info"), "-c", build_path("comment"), "-d", build_path("description"), "-f", build_path("packlist"), "-I", "/opt/local", "-p", staging_path, "-U", "#{cwd}/#{name}-#{self.version}-#{iteration}.tgz" ]
26
+ safesystem("pkg_create", *args)
27
+
28
+ end
29
+
30
+ def iteration
31
+ return @iteration ? @iteration : 1
32
+ end
33
+
34
+ end
35
+
@@ -1,11 +1,23 @@
1
1
  from distutils.core import Command
2
2
  import os
3
+ import sys
3
4
  import pkg_resources
4
5
  try:
5
6
  import json
6
7
  except ImportError:
7
8
  import simplejson as json
8
9
 
10
+ PY3 = sys.version_info[0] == 3
11
+
12
+ if PY3:
13
+ def u(s):
14
+ return s
15
+ else:
16
+ def u(s):
17
+ if isinstance(u, unicode):
18
+ return u
19
+ return s.decode('utf-8')
20
+
9
21
 
10
22
  # Note, the last time I coded python daily was at Google, so it's entirely
11
23
  # possible some of my techniques below are outdated or bad.
@@ -17,12 +29,14 @@ class get_metadata(Command):
17
29
  user_options = [
18
30
  ('load-requirements-txt', 'l',
19
31
  "load dependencies from requirements.txt"),
20
- ]
32
+ ("output=", "o", "output destination for metadata json")
33
+ ]
21
34
  boolean_options = ['load-requirements-txt']
22
35
 
23
36
  def initialize_options(self):
24
37
  self.load_requirements_txt = False
25
38
  self.cwd = None
39
+ self.output = None
26
40
 
27
41
  def finalize_options(self):
28
42
  self.cwd = os.getcwd()
@@ -46,9 +60,9 @@ class get_metadata(Command):
46
60
  data = {
47
61
  "name": self.distribution.get_name(),
48
62
  "version": self.distribution.get_version(),
49
- "author": "%s <%s>" % (
50
- self.distribution.get_author(),
51
- self.distribution.get_author_email(),
63
+ "author": u("%s <%s>") % (
64
+ u(self.distribution.get_author()),
65
+ u(self.distribution.get_author_email()),
52
66
  ),
53
67
  "description": self.distribution.get_description(),
54
68
  "license": self.distribution.get_license(),
@@ -74,8 +88,9 @@ class get_metadata(Command):
74
88
 
75
89
  data["dependencies"] = final_deps
76
90
 
91
+ output = open(self.output, "w")
77
92
  if hasattr(json, 'dumps'):
78
- print(json.dumps(data, indent=2))
93
+ output.write(json.dumps(data, indent=2))
79
94
  else:
80
95
  # For Python 2.5 and Debian's python-json
81
- print(json.write(data))
96
+ output.write(json.write(data))
@@ -55,12 +55,12 @@ class FPM::Package::Python < FPM::Package
55
55
  "Want to what your target platform is using? Run this: " \
56
56
  "python -c 'from distutils.sysconfig import get_python_lib; " \
57
57
  "print get_python_lib()'"
58
- option "--install-data", "DATA_PATH", "The path to where data should be." \
58
+ option "--install-data", "DATA_PATH", "The path to where data should be " \
59
59
  "installed to. This is equivalent to 'python setup.py --install-data " \
60
60
  "DATA_PATH"
61
61
  option "--dependencies", :flag, "Include requirements defined in setup.py" \
62
62
  " as dependencies.", :default => true
63
- option "--obey-requirements-txt", :flag, "Use a requirements.txt file" \
63
+ option "--obey-requirements-txt", :flag, "Use a requirements.txt file " \
64
64
  "in the top-level directory of the python package for dependency " \
65
65
  "detection.", :default => false
66
66
 
@@ -148,8 +148,9 @@ class FPM::Package::Python < FPM::Package
148
148
  setup_dir = File.dirname(setup_py)
149
149
 
150
150
  output = ::Dir.chdir(setup_dir) do
151
+ tmp = build_path("metadata.json")
151
152
  setup_cmd = "env PYTHONPATH=#{pylib} #{attributes[:python_bin]} " \
152
- "setup.py --command-packages=pyfpm get_metadata"
153
+ "setup.py --command-packages=pyfpm get_metadata --output=#{tmp}"
153
154
 
154
155
  if attributes[:python_obey_requirements_txt?]
155
156
  setup_cmd += " --load-requirements-txt"
@@ -160,16 +161,17 @@ class FPM::Package::Python < FPM::Package
160
161
  # details.
161
162
  @logger.info("fetching package metadata", :setup_cmd => setup_cmd)
162
163
 
163
- output = %x{#{setup_cmd}}
164
- if !$?.success?
164
+ success = safesystem(setup_cmd)
165
+ #%x{#{setup_cmd}}
166
+ if !success
165
167
  @logger.error("setup.py get_metadata failed", :command => setup_cmd,
166
168
  :exitcode => $?.exitstatus)
167
169
  raise "An unexpected error occurred while processing the setup.py file"
168
170
  end
169
- output
171
+ File.read(tmp)
170
172
  end
171
- @logger.debug("full text from `setup.py get_metadata`", :data => output)
172
- metadata = JSON.parse(output[/\{.*\}/msx])
173
+ @logger.debug("result from `setup.py get_metadata`", :data => output)
174
+ metadata = JSON.parse(output)
173
175
  @logger.info("object output of get_metadata", :json => metadata)
174
176
 
175
177
  self.architecture = metadata["architecture"]
@@ -113,6 +113,11 @@ class FPM::Package::RPM < FPM::Package
113
113
  next rpmbuild_filter_from_requires
114
114
  end
115
115
 
116
+ option "--ignore-iteration-in-dependencies", :flag,
117
+ "For '=' (equal) dependencies, allow iterations on the specified " \
118
+ "version. Default is to be specific. This option allows the same " \
119
+ "version of a package but any iteration is permitted"
120
+
116
121
  private
117
122
 
118
123
  # Fix path name
@@ -129,14 +134,18 @@ class FPM::Package::RPM < FPM::Package
129
134
  end
130
135
 
131
136
  def rpm_file_entry(file)
137
+ original_file = file
132
138
  file = rpm_fix_name(file)
133
139
  return file unless attributes[:rpm_use_file_permissions?]
134
140
 
135
- stat = File.stat(file.gsub(/\"/, '').sub(/^\//,''))
136
- user = Etc.getpwuid(stat.uid).name
137
- group = Etc.getgrgid(stat.gid).name
138
- mode = stat.mode
139
- return sprintf("%%attr(%o, %s, %s) %s\n", mode & 4095 , user, group, file)
141
+ # Stat the original filename in the relative staging path
142
+ ::Dir.chdir(staging_path) do
143
+ stat = File.stat(original_file.gsub(/\"/, '').sub(/^\//,''))
144
+ user = Etc.getpwuid(stat.uid).name
145
+ group = Etc.getgrgid(stat.gid).name
146
+ mode = stat.mode
147
+ return sprintf("%%attr(%o, %s, %s) %s\n", mode & 4095 , user, group, file)
148
+ end
140
149
  end
141
150
 
142
151
 
@@ -184,25 +193,23 @@ class FPM::Package::RPM < FPM::Package
184
193
  # Convert 'rubygem-foo' provides values to 'rubygem(foo)'
185
194
  # since that's what most rpm packagers seem to do.
186
195
  self.provides = self.provides.collect do |provides|
187
- first, remainder = provides.split("-", 2)
188
- if first == "rubygem"
189
- name, remainder = remainder.split(" ", 2)
190
- # yield rubygem(name)...
191
- "rubygem(#{name})#{remainder ? " #{remainder}" : ""}"
196
+ # Tries to match rubygem_prefix [1], gem_name [2] and version [3] if present
197
+ # and return it in rubygem_prefix(gem_name) form
198
+ if name=/^(#{attributes[:gem_package_name_prefix]})-([^\s]+)\s*(.*)$/.match(provides)
199
+ "#{name[1]}(#{name[2]})#{name[3] ? " #{name[3]}" : ""}"
192
200
  else
193
201
  provides
194
202
  end
195
203
  end
196
204
  self.dependencies = self.dependencies.collect do |dependency|
197
- first, remainder = dependency.split("-", 2)
198
- if first == "rubygem"
199
- name, remainder = remainder.split(" ", 2)
200
- "rubygem(#{name})#{remainder ? " #{remainder}" : ""}"
205
+ # Tries to match rubygem_prefix [1], gem_name [2] and version [3] if present
206
+ # and return it in rubygem_prefix(gem_name) form
207
+ if name=/^(#{attributes[:gem_package_name_prefix]})-([^\s]+)\s*(.*)$/.match(dependency)
208
+ "#{name[1]}(#{name[2]})#{name[3] ? " #{name[3]}" : ""}"
201
209
  else
202
210
  dependency
203
211
  end
204
212
  end
205
- #self.provides << "rubygem(#{self.name})"
206
213
  end
207
214
 
208
215
  # Convert != dependency as Conflict =, as rpm doesn't understand !=
@@ -229,6 +236,23 @@ class FPM::Package::RPM < FPM::Package
229
236
  end
230
237
  end
231
238
 
239
+ # if --ignore-iteration-in-dependencies is true convert foo = X, to
240
+ # foo >= X , foo < X+1
241
+ if self.attributes[:rpm_ignore_iteration_in_dependencies?]
242
+ self.dependencies = self.dependencies.collect do |dep|
243
+ name, op, version = dep.split(/\s+/)
244
+ if op == '='
245
+ nextversion = version.split('.').collect { |v| v.to_i }
246
+ nextversion[-1] += 1
247
+ nextversion = nextversion.join(".")
248
+ @logger.warn("Converting dependency #{dep} to #{name} >= #{version}, #{name} < #{nextversion}")
249
+ ["#{name} >= #{version}", "#{name} < #{nextversion}"]
250
+ else
251
+ dep
252
+ end
253
+ end.flatten
254
+ end
255
+
232
256
  end # def converted
233
257
 
234
258
  def input(path)
@@ -316,7 +340,7 @@ class FPM::Package::RPM < FPM::Package
316
340
 
317
341
  Find.find(staging_path) do |path|
318
342
  next if path == staging_path
319
- if File.directory? path
343
+ if File.directory? path and !File.symlink? path
320
344
  add_path = path.gsub(/^#{staging_path}/,'')
321
345
  self.directories << add_path if not fs_dirs.include? add_path
322
346
  end
@@ -326,7 +350,7 @@ class FPM::Package::RPM < FPM::Package
326
350
  alldirs = []
327
351
  self.directories.each do |path|
328
352
  Find.find(File.join(staging_path, path)) do |subpath|
329
- if File.directory? subpath
353
+ if File.directory? subpath and !File.symlink? subpath
330
354
  alldirs << subpath.gsub(/^#{staging_path}/, '')
331
355
  end
332
356
  end
@@ -334,12 +358,29 @@ class FPM::Package::RPM < FPM::Package
334
358
  self.directories = alldirs
335
359
  end
336
360
 
337
- self.config_files = self.config_files.map { |x| File.join(self.prefix, x) }
361
+ # scan all conf file paths for files and add them
362
+ allconfigs = []
363
+ self.config_files.each do |path|
364
+ cfg_path = File.expand_path(path, staging_path)
365
+ Find.find(cfg_path) do |p|
366
+ allconfigs << p.gsub("#{staging_path}/", '') if File.file? p
367
+ end
368
+ end
369
+ allconfigs.sort!.uniq!
370
+
371
+ self.config_files = allconfigs.map { |x| File.join(self.prefix, x) }
338
372
 
339
373
  (attributes[:rpm_rpmbuild_define] or []).each do |define|
340
374
  args += ["--define", define]
341
375
  end
342
376
 
377
+ # copy all files from staging to BUILD dir
378
+ Find.find(staging_path) do |path|
379
+ src = path.gsub(/^#{staging_path}/, '')
380
+ dst = File.join(build_path, build_sub_dir, src)
381
+ copy_entry(path, dst)
382
+ end
383
+
343
384
  rpmspec = template("rpm.erb").result(binding)
344
385
  specfile = File.join(build_path("SPECS"), "#{name}.spec")
345
386
  File.write(specfile, rpmspec)
@@ -1,8 +1,21 @@
1
1
  require "fpm/namespace"
2
2
  require "childprocess"
3
+ require "ffi"
3
4
 
4
5
  # Some utility functions
5
6
  module FPM::Util
7
+ extend FFI::Library
8
+ ffi_lib FFI::Library::LIBC
9
+
10
+ # mknod is __xmknod in glibc a wrapper around mknod to handle
11
+ # various stat struct formats. See bits/stat.h in glibc source
12
+ begin
13
+ attach_function :mknod, :mknod, [:string, :uint, :ulong], :int
14
+ rescue FFI::NotFoundError
15
+ # glibc/io/xmknod.c int __xmknod (int vers, const char *path, mode_t mode, dev_t *dev)
16
+ attach_function :xmknod, :__xmknod, [:int, :string, :uint, :pointer], :int
17
+ end
18
+
6
19
  # Raised if safesystem cannot find the program to run.
7
20
  class ExecutableNotFound < StandardError; end
8
21
 
@@ -46,7 +59,9 @@ module FPM::Util
46
59
  process.start
47
60
  stdout_w.close; stderr_w.close
48
61
  @logger.debug('Process is running', :pid => process.pid)
49
- @logger.pipe(stdout_r => :info, stderr_r => :error)
62
+ # Log both stdout and stderr as 'info' because nobody uses stderr for
63
+ # actually reporting errors and as a result 'stderr' is a misnomer.
64
+ @logger.pipe(stdout_r => :info, stderr_r => :info)
50
65
 
51
66
  process.wait
52
67
  success = (process.exit_code == 0)
@@ -113,4 +128,30 @@ module FPM::Util
113
128
  def with(value, &block)
114
129
  block.call(value)
115
130
  end # def with
131
+
132
+ # wrapper around mknod ffi calls
133
+ def mknod_w(path, mode, dev)
134
+ rc = -1
135
+ case %x{uname -s}.chomp
136
+ when 'Linux'
137
+ # bits/stat.h #define _MKNOD_VER_LINUX 0
138
+ rc = xmknod(0, path, mode, FFI::MemoryPointer.new(dev))
139
+ else
140
+ rc = mknod(path, mode, dev)
141
+ end
142
+ rc
143
+ end
144
+
145
+ def copy_entry(src, dst)
146
+ case File.ftype(src)
147
+ when 'fifo', 'characterSpecial', 'blockSpecial', 'socket'
148
+ st = File.stat(src)
149
+ rc = mknod_w(dst, st.mode, st.dev)
150
+ raise SystemCallError.new("mknod error", FFI.errno) if rc == -1
151
+ when 'directory'
152
+ FileUtils.mkdir(dst) unless File.exists? dst
153
+ else
154
+ FileUtils.copy_entry(src, dst)
155
+ end
156
+ end
116
157
  end # module FPM::Util