fpm 0.4.42 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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