fpm 1.16.0 → 1.17.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: f6f1fc4a26f95d77ce3626231e5b7674b8908fe7afc7e2c524f573852291ed2b
4
- data.tar.gz: e31fa7d3a39de85c3a75b847fc9c336e5096ca32ecd08720554f3057e73e28ef
3
+ metadata.gz: ef536c2af546fea798392b6cca4c07b81b5f9a3d52654e72c5cb9543d1d4a21d
4
+ data.tar.gz: 95a4ea389bb037d2555ea95fd99313897b0f30c5b3bb8c5519a19d330b1e2842
5
5
  SHA512:
6
- metadata.gz: f6988868dc8fae9930af5163495bb76d4afa631b58d8608f903349aefd253321ab35d88ad7b5445793b6d6d378ca941bb65d3b3353244511d32d5c13bd22c8fa
7
- data.tar.gz: fd0660808bb60d2796bc80fe25c5fd3e63f942ce24f79c4591e2d7c1917ef42af6529db07d116677ba439283843a99a80d8d5615c787815cd3a44a3540a5e474
6
+ metadata.gz: ccd36e905583145a6a80ca5efc62388ea2abdc1e35a8d3a077b5f946f2d2ea864a282fac0e77f9b5c0c82af09a96e6ae84067a6424b6d0d88f56282eea888111
7
+ data.tar.gz: 6c4a93a51fe6af403ecda1910466b41843a8508837cea3f68042e56dba1083543f61a45b14d560167063b265279da57166368471dfc32bd8837c5e9d65847ed7
data/CHANGELOG.rst CHANGED
@@ -1,6 +1,18 @@
1
1
  Release Notes and Change Log
2
2
  ============================
3
3
 
4
+ 1.17.0 (October 2, 2025)
5
+ ^^^^^^^^^^^^^^^^^^^^^^^^
6
+
7
+ * python: Support modern Python project features: pyproject.toml, wheels, etc. Now, any project that can be built or installed with ``pip`` can be packaged by fpm. Previously, fpm relied on a long-deprecated features in setup.py to see a python project's metadata such as name, version, and dependencies. Fpm now uses python's package tools to identify the project's name, version, dependencies, and other information. (`#2104`_, `#2105`_, `#2040`_, `#1982`_, `#2029`_; Jordan Sissel, cwegener, amdei, gmabey)
8
+ * dir: When copying files, only use hardlinks if the original files were also hardlinks. (`#2103`_, `#2102`_; Michael Telatynski, Matthew Rathbone, Jordan Sissel). :w
9
+ Related: https://github.com/electron-userland/electron-builder/issues/5721
10
+ * deb: bug fix: when a file given with ``--config-files <path>`` copied into the package, fpm was forgetting to mark the file as being a config file in the package, aka Debian "conffiles" (`#2027`_, `#1823`_; Alexandr Zarubkin, Kientz Arnaud)
11
+ * pacman: Now can build packages with aarch64 and arm7hf architecture (`#2017`_; Markson Hon)
12
+ * rpm: Paths with '{' and '}' characters can now be included in rpms (`#2088`_ `#2087`_; Jordan Sissel, Manish2481983)
13
+ * docs: Updated urls which pointed at rpm documentation (`#2092`_, `#2011`_, `#2054`_; André Kelpe, Natanael Arndt)
14
+ * Ruby 3.4.0 no longer gives warnings related to `ostruct` (`#2106`_, also `#2104`_ and `#2103`_; Jordan Sissel)
15
+
4
16
  1.16.0 (December 8, 2024)
5
17
  ^^^^^^^^^^^^^^^^^^^^^^^^^
6
18
 
data/lib/fpm/command.rb CHANGED
@@ -3,7 +3,6 @@ require "fpm/namespace"
3
3
  require "fpm/version"
4
4
  require "fpm/util"
5
5
  require "clamp"
6
- require "ostruct"
7
6
  require "fpm"
8
7
  require "tmpdir" # for Dir.tmpdir
9
8
 
@@ -260,7 +259,11 @@ class FPM::Command < Clamp::Command
260
259
  "'gem', it specifies the packages to download and use as the gem input",
261
260
  :attribute_name => :args
262
261
 
262
+ # Keep a copy of the original flags (ones declared above, not by package types)
263
+ # This helps when generating the documentation
264
+ GENERAL_OPTIONS = @declared_options.clone
263
265
  FPM::Package.types.each do |name, klass|
266
+ # This adds each package's flags to the main command
264
267
  klass.apply_options(self)
265
268
  end
266
269
 
@@ -1151,6 +1151,7 @@ class FPM::Package::Deb < FPM::Package
1151
1151
  logger.debug("Adding config file #{path} to Staging area #{staging_path}")
1152
1152
  FileUtils.mkdir_p(File.dirname(dcl))
1153
1153
  FileUtils.cp_r path, dcl
1154
+ add_path(path, allconfigs)
1154
1155
  else
1155
1156
  logger.debug("Config file aready exists in staging area.")
1156
1157
  end
@@ -197,10 +197,6 @@ class FPM::Package::Dir < FPM::Package
197
197
  else
198
198
  # Otherwise try copying the file.
199
199
  begin
200
- logger.debug("Linking", :source => source, :destination => destination)
201
- File.link(source, destination)
202
- rescue Errno::ENOENT, Errno::EXDEV, Errno::EPERM
203
- # Hardlink attempt failed, copy it instead
204
200
  logger.debug("Copying", :source => source, :destination => destination)
205
201
  copy_entry(source, destination)
206
202
  rescue Errno::EEXIST
@@ -37,11 +37,15 @@ class FPM::Package::Pacman < FPM::Package
37
37
  def architecture
38
38
  case @architecture
39
39
  when nil
40
- return %x{uname -m}.chomp # default to current arch
41
- when "amd64" # debian and pacman disagree on architecture names
42
- return "x86_64"
40
+ return %x{uname -m}.chomp # default to current arch
41
+ when "amd64" # Debian uses amd64
42
+ return "x86_64" # Arch Linux uses x86_64
43
+ when "arm64" # Debian uses arm64
44
+ return "aarch64" # Arch Linux ARM uses aarch64
45
+ when "armhf" # Debian uses armhf
46
+ return "arm7hf" # Arch Linux ARM uses arm7hf
43
47
  when "native"
44
- return %x{uname -m}.chomp # 'native' is current arch
48
+ return %x{uname -m}.chomp # 'native' is the current arch
45
49
  when "all", "any", "noarch"
46
50
  return "any"
47
51
  else
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env python3
2
+
3
+ #import pkg_resources
4
+ import packaging.requirements
5
+ import json
6
+ import sys
7
+
8
+ # Expect requirements lines via stdin.
9
+ #requirements = pkg_resources.parse_requirements(sys.stdin)
10
+
11
+ # Process environment markers, if any, and produce a list of requirements for the current environment.
12
+ def evaluate_requirements(fd):
13
+ all_requirements = [packaging.requirements.Requirement(line) for line in sys.stdin]
14
+
15
+ for req in all_requirements:
16
+ # XXX: Note: marker.evaluate() can be given a dict() containing environment values to overwrite
17
+ if req.marker is None or req.marker.evaluate():
18
+ if len(req.specifier) > 0:
19
+ for spec in req.specifier:
20
+ yield "%s%s" % (req.name, spec)
21
+ else:
22
+ yield str(req.name)
23
+
24
+ print(json.dumps(list(evaluate_requirements(sys.stdin))))
@@ -51,76 +51,371 @@ class FPM::Package::Python < FPM::Package
51
51
  option "--downcase-dependencies", :flag, "Should the package dependencies " \
52
52
  "be in lowercase?", :default => true
53
53
 
54
- option "--install-bin", "BIN_PATH", "The path to where python scripts " \
55
- "should be installed to."
56
- option "--install-lib", "LIB_PATH", "The path to where python libs " \
54
+ option "--install-bin", "BIN_PATH", "(DEPRECATED, does nothing) The path to where python scripts " \
55
+ "should be installed to." do
56
+ logger.warn("Using deprecated flag --install-bin")
57
+ end
58
+ option "--install-lib", "LIB_PATH", "(DEPRECATED, does nothing) The path to where python libs " \
57
59
  "should be installed to (default depends on your python installation). " \
58
60
  "Want to find out what your target platform is using? Run this: " \
59
61
  "python -c 'from distutils.sysconfig import get_python_lib; " \
60
- "print get_python_lib()'"
61
- option "--install-data", "DATA_PATH", "The path to where data should be " \
62
+ "print get_python_lib()'" do
63
+ logger.warn("Using deprecated flag --install-bin")
64
+ end
65
+
66
+ option "--install-data", "DATA_PATH", "(DEPRECATED, does nothing) The path to where data should be " \
62
67
  "installed to. This is equivalent to 'python setup.py --install-data " \
63
- "DATA_PATH"
64
- option "--dependencies", :flag, "Include requirements defined in setup.py" \
68
+ "DATA_PATH" do
69
+ logger.warn("Using deprecated flag --install-bin")
70
+ end
71
+
72
+ option "--dependencies", :flag, "Include requirements defined by the python package" \
65
73
  " as dependencies.", :default => true
66
74
  option "--obey-requirements-txt", :flag, "Use a requirements.txt file " \
67
75
  "in the top-level directory of the python package for dependency " \
68
76
  "detection.", :default => false
69
- option "--scripts-executable", "PYTHON_EXECUTABLE", "Set custom python " \
77
+ option "--scripts-executable", "PYTHON_EXECUTABLE", "(DEPRECATED) Set custom python " \
70
78
  "interpreter in installing scripts. By default distutils will replace " \
71
79
  "python interpreter in installing scripts (specified by shebang) with " \
72
80
  "current python interpreter (sys.executable). This option is equivalent " \
73
81
  "to appending 'build_scripts --executable PYTHON_EXECUTABLE' arguments " \
74
- "to 'setup.py install' command."
82
+ "to 'setup.py install' command." do
83
+ logger.warn("Using deprecated flag --install-bin")
84
+ end
85
+
75
86
  option "--disable-dependency", "python_package_name",
76
87
  "The python package name to remove from dependency list",
77
88
  :multivalued => true, :attribute_name => :python_disable_dependency,
78
89
  :default => []
79
90
  option "--setup-py-arguments", "setup_py_argument",
80
- "Arbitrary argument(s) to be passed to setup.py",
91
+ "(DEPRECATED) Arbitrary argument(s) to be passed to setup.py",
81
92
  :multivalued => true, :attribute_name => :python_setup_py_arguments,
82
- :default => []
93
+ :default => [] do
94
+ logger.warn("Using deprecated flag --install-bin")
95
+ end
83
96
  option "--internal-pip", :flag,
84
97
  "Use the pip module within python to install modules - aka 'python -m pip'. This is the recommended usage since Python 3.4 (2014) instead of invoking the 'pip' script",
85
98
  :attribute_name => :python_internal_pip,
86
99
  :default => true
100
+
101
+ class PythonMetadata
102
+ require "strscan"
103
+
104
+ class MissingField < StandardError; end
105
+ class UnexpectedContent < StandardError; end
106
+
107
+ # According to https://packaging.python.org/en/latest/specifications/core-metadata/
108
+ # > Core Metadata v2.4 - August 2024
109
+ MULTIPLE_USE = %w(Dynamic Platform Supported-Platform License-File Classifier Requires-Dist Requires-External Project-URL Provides-Extra Provides-Dist Obsoletes-Dist)
110
+
111
+ # METADATA files are described in Python Packaging "Core Metadata"[1] and appear to have roughly RFC822 syntax.
112
+ # [1] https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata
113
+ def self.parse(input)
114
+ s = StringScanner.new(input)
115
+ headers = {}
116
+
117
+ # Default "Multiple use" fields to empty array instead of nil.
118
+ MULTIPLE_USE.each do |field|
119
+ headers[field] = []
120
+ end
121
+
122
+ while !s.eos? and !s.scan("\n") do
123
+ # Field is non-space up, but excluding the colon
124
+ field = s.scan(/[^\s:]+/)
125
+
126
+ # Skip colon and following whitespace
127
+ s.scan(/:\s*/)
128
+
129
+ # Value is text until newline, and any following lines if they have leading spaces.
130
+ value = s.scan(/[^\n]+(?:\Z|\n(?:[ \t][^\n]+\n)*)/)
131
+ if value.nil?
132
+ raise "Failed parsing Python package metadata value at field #{field}, char offset #{s.pos}"
133
+ end
134
+ value = value.chomp
135
+
136
+ if MULTIPLE_USE.include?(field)
137
+ raise "Header field should be an array. This is a bug in fpm." if !headers[field].is_a?(Array)
138
+ headers[field] << value
139
+ else
140
+ headers[field] = value
141
+ end
142
+ end # while reading headers
143
+
144
+ # If there's more content beyond the last header, then it's a content body.
145
+ # In Python Metadata >= 2.1, the descriptino can be written in the body.
146
+ if !s.eos?
147
+ if headers["Metadata-Version"].to_f >= 2.1
148
+ # Per Python core-metadata spec:
149
+ # > Changed in version 2.1: This field may be specified in the message body instead.
150
+ #return PythonMetadata.new(headers, s.string[s.pos ...])
151
+ return headers, s.string[s.pos ... ]
152
+ elsif headers["Metadata-Version"].to_f >= 2.0
153
+ # dnspython v1.15.0 has a description body and Metadata-Version 2.0
154
+ # this seems out of spec, but let's accept it anyway.
155
+ return headers, s.string[s.pos ... ]
156
+ else
157
+ raise "After reading METADATA headers, extra data is in the file but was not expected. This may be a bug in fpm."
158
+ end
159
+ end
160
+
161
+ #return PythonMetadata.new(headers)
162
+ return headers, nil # nil means no body in this metadata
163
+ rescue => e
164
+ puts "String scan failed: #{e}"
165
+ puts "Position: #{s.pointer}"
166
+ puts "---"
167
+ puts input
168
+ puts "==="
169
+ puts input[s.pointer...]
170
+ puts "---"
171
+ raise e
172
+ end # self.parse
173
+
174
+ def self.from(input)
175
+ return PythonMetadata.new(*parse(input))
176
+ end
87
177
 
88
- private
178
+ # Only focusing on terms fpm may care about
179
+ attr_reader :name, :version, :summary, :description, :keywords, :maintainer, :license, :requires, :homepage
180
+
181
+ FIELD_MAP = {
182
+ :@name => "Name",
183
+ :@version => "Version",
184
+ :@summary => "Summary",
185
+ :@description => "Description",
186
+ :@keywords => "Keywords",
187
+ :@maintainer => "Author-email",
188
+
189
+ # Note: License can also come from the deprecated "License" field
190
+ # This is processed later in this method.
191
+ :@license => "License-Expression",
192
+
193
+ :@requires => "Requires-Dist",
194
+ }
195
+
196
+ REQUIRED_FIELDS = [ "Metadata-Version", "Name", "Version" ]
197
+
198
+ # headers - a Hash containing field-value pairs from headers as read from a python METADATA file.
199
+ # body - optional, a string containing the body text of a METADATA file
200
+ def initialize(headers, body=nil)
201
+ REQUIRED_FIELDS.each do |field|
202
+ if !headers.include?(field)
203
+ raise MissingField, "Missing required Python metadata field, '#{field}'. This might be a bug in the package or in fpm."
204
+ end
205
+ end
206
+
207
+ FIELD_MAP.each do |attr, field|
208
+ if headers.include?(field)
209
+ instance_variable_set(attr, headers.fetch(field))
210
+ end
211
+ end
212
+
213
+ # Do any extra processing on fields to turn them into their expected content.
214
+ process_description(headers, body)
215
+ process_license(headers)
216
+ process_homepage(headers)
217
+ process_maintainer(headers)
218
+ end # def initialize
219
+
220
+ private
221
+ def process_description(headers, body)
222
+ if @description
223
+ # Per python core-metadata spec:
224
+ # > To support empty lines and lines with indentation with respect to the
225
+ # > RFC 822 format, any CRLF character has to be suffixed by 7 spaces
226
+ # > followed by a pipe (“|”) char. As a result, the Description field is
227
+ # > encoded into a folded field that can be interpreted by RFC822 parser [2].
228
+ @description = @description.gsub!(/^ |/, "")
229
+ end
230
+
231
+ if !body.nil?
232
+ if headers["Metadata-Version"].to_f >= 2.1
233
+ # Per Python core-metadata spec:
234
+ # > Changed in version 2.1: [Description] field may be specified in the message body instead.
235
+ #
236
+ # The description is simply the rest of the METADATA file after the headers.
237
+ @description = body
238
+ elsif headers["Metadata-Version"].to_f >= 2.0
239
+ # dnspython v1.15.0 has a description body and Metadata-Version 2.0
240
+ # this seems out of spec, but let's accept it anyway.
241
+ @description = body
242
+ else
243
+ raise UnexpectedContent, "Found a content body in METADATA file, but Metadata-Version(#{headers["Metadata-Version"]}) is below 2.1 and doesn't support this. This may be a bug in fpm or a malformed python package."
244
+ end
245
+
246
+ # What to do if we find a description body but already have a Description field set in the headers?
247
+ if headers.include?("Description")
248
+ raise "Found a description in the body of the python package metadata, but the package already set the Description field. I don't know what to do. This is probably a bug in fpm."
249
+ end
250
+ end
251
+
252
+ # XXX: The description field can be markdown, plain text, or reST.
253
+ # Content type is noted in the "Description-Content-Type" field
254
+ # Should we transform this to plain text?
255
+ end # process_description
256
+
257
+ def process_license(headers)
258
+ # Ignore the "License" field if License-Expression is also present.
259
+ return if headers["Metadata-Version"].to_f >= 2.4 && headers.include?("License-Expression")
260
+
261
+ # Deprecated field, License, as described in python core-metadata:
262
+ # > As of Metadata 2.4, License and License-Expression are mutually exclusive.
263
+ # > If both are specified, tools which parse metadata will disregard License
264
+ # > and PyPI will reject uploads. See PEP 639.
265
+ if headers["License"]
266
+ # Note: This license can be free form text, so it's unclear if it's a great choice.
267
+ # however, the original python metadata License field is quite old/deprecated
268
+ # so maybe nobody uses it anymore?
269
+ @license = headers["License"]
270
+ elsif license_classifier = headers["Classifier"].find { |value| value =~ /^License ::/ }
271
+ # The license could also show up in the "Classifier" header with "License ::" as a prefix.
272
+ @license = license_classifier.sub(/^License ::/, "")
273
+ end # check for deprecated License field
274
+ end # process_license
275
+
276
+ def process_homepage(headers)
277
+ return if headers["Project-URL"].empty?
278
+
279
+ # Create a hash of Project-URL where the label is the key, url the value.
280
+ urls = Hash[*headers["Project-URL"].map do |text|
281
+ label, url = text.split(/, */, 2)
282
+ # Normalize the label by removing punctuation and spaces
283
+ # Reference: https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization
284
+ # > In plain language: a label is normalized by deleting all ASCII punctuation and whitespace, and then converting the result to lowercase.
285
+ label = label.gsub(/[[:punct:][:space:]]/, "").downcase
286
+ [label, url]
287
+ end.flatten(1)]
288
+
289
+ # Prioritize certain URL labels when choosing the homepage url.
290
+ [ "homepage", "source", "documentation", "releasenotes" ].each do |label|
291
+ if urls.include?(label)
292
+ @homepage = urls[label]
293
+ end
294
+ end
295
+
296
+ # Otherwise, default to the first URL
297
+ @homepage = urls.values.first
298
+ end
299
+
300
+ def process_maintainer(headers)
301
+ # Python metadata supports both "Author-email" and "Maintainer-email"
302
+ # Of the "Maintainer" fields, python core-metadata says:
303
+ # > Note that this field is intended for use when a project is being maintained by someone other than the original author
304
+ #
305
+ # So we should prefer Maintainer-email if it exists, but fall back to Author-email otherwise.
306
+ @maintainer = headers["Maintainer-email"] unless headers["Maintainer-email"].nil?
307
+ end
308
+ end # class PythonMetadata
89
309
 
90
310
  # Input a package.
91
311
  #
92
312
  # The 'package' can be any of:
93
313
  #
94
314
  # * A name of a package on pypi (ie; easy_install some-package)
95
- # * The path to a directory containing setup.py
96
- # * The path to a setup.py
315
+ # * The path to a directory containing setup.py or pyproject.toml
316
+ # * The path to a setup.py or pyproject.toml
317
+ # * The path to a python sdist file ending in .tar.gz
318
+ # * The path to a python wheel file ending in .whl
97
319
  def input(package)
320
+ explore_environment
321
+
98
322
  path_to_package = download_if_necessary(package, version)
99
323
 
324
+ # Expect a setup.py or pyproject.toml if it's a directory.
100
325
  if File.directory?(path_to_package)
101
- setup_py = File.join(path_to_package, "setup.py")
102
- else
103
- setup_py = path_to_package
326
+ if !(File.exist?(File.join(path_to_package, "setup.py")) or File.exist?(File.join(path_to_package, "pyproject.toml")))
327
+ raise FPM::InvalidPackageConfiguration, "The path ('#{path_to_package}') doesn't appear to be a python package directory. I expected either a pyproject.toml or setup.py but found neither."
328
+ end
104
329
  end
105
330
 
106
- if !File.exist?(setup_py)
107
- logger.error("Could not find 'setup.py'", :path => setup_py)
108
- raise "Unable to find python package; tried #{setup_py}"
331
+ if File.file?(path_to_package)
332
+ if ["setup.py", "pyproject.toml"].include?(File.basename(path_to_package))
333
+ path_to_package = File.dirname(path_to_package)
334
+ end
109
335
  end
110
336
 
111
- load_package_info(setup_py)
112
- install_to_staging(setup_py)
337
+ if [".tar.gz", ".tgz"].any? { |suffix| path_to_package.end_with?(suffix) }
338
+ # Have pip convert the .tar.gz (source dist?) into a wheel
339
+ logger.debug("Found tarball and assuming it's a python source package.")
340
+ safesystem(*attributes[:python_pip], "wheel", "--no-deps", "-w", build_path, path_to_package)
341
+
342
+ path_to_package = ::Dir.glob(build_path("*.whl")).first
343
+ if path_to_package.nil?
344
+ raise FPM::InvalidPackageConfiguration, "Failed building python package format - fpm tried to build a python wheel, but didn't find the .whl file. This might be a bug in fpm."
345
+ end
346
+ elsif File.directory?(path_to_package)
347
+ logger.debug("Found directory and assuming it's a python source package.")
348
+ safesystem(*attributes[:python_pip], "wheel", "--no-deps", "-w", build_path, path_to_package)
349
+
350
+ if attributes[:python_obey_requirements_txt?]
351
+ reqtxt = File.join(path_to_package, "requirements.txt")
352
+ @requirements_txt = File.read(reqtxt).split("\n") if File.file?(reqtxt)
353
+ end
354
+
355
+ path_to_package = ::Dir.glob(build_path("*.whl")).first
356
+ if path_to_package.nil?
357
+ raise FPM::InvalidPackageConfiguration, "Failed building python package format - fpm tried to build a python wheel, but didn't find the .whl file. This might be a bug in fpm."
358
+ end
359
+
360
+ end
361
+
362
+ load_package_info(path_to_package)
363
+ install_to_staging(path_to_package)
113
364
  end # def input
114
365
 
366
+ def explore_environment
367
+ if !attributes[:python_bin_given?]
368
+ # If --python-bin isn't set, try to find a good default python executable path, because it might not be "python"
369
+ pythons = [ "python", "python3", "python2" ]
370
+ default_python = pythons.find { |py| program_exists?(py) }
371
+
372
+ if default_python.nil?
373
+ raise FPM::Util::ExecutableNotFound, "Could not find any python interpreter. Tried the following: #{pythons.join(", ")}"
374
+ end
375
+
376
+ logger.info("Setting default python executable", :name => default_python)
377
+ attributes[:python_bin] = default_python
378
+
379
+ if !attributes[:python_package_name_prefix_given?]
380
+ attributes[:python_package_name_prefix] = default_python
381
+ logger.info("Setting package name prefix", :name => default_python)
382
+ end
383
+ end
384
+
385
+ if attributes[:python_internal_pip?]
386
+ # XXX: Should we detect if internal pip is available?
387
+ attributes[:python_pip] = [ attributes[:python_bin], "-m", "pip"]
388
+ end
389
+ end # explore_environment
390
+
391
+
392
+
115
393
  # Download the given package if necessary. If version is given, that version
116
394
  # will be downloaded, otherwise the latest is fetched.
117
395
  def download_if_necessary(package, version=nil)
118
- # TODO(sissel): this should just be a 'download' method, the 'if_necessary'
119
- # part should go elsewhere.
120
396
  path = package
397
+
121
398
  # If it's a path, assume local build.
122
- if File.directory?(path) or (File.exist?(path) and File.basename(path) == "setup.py")
123
- return path
399
+ if File.exist?(path)
400
+ return path if File.directory?(path)
401
+
402
+ basename = File.basename(path)
403
+ return File.dirname(path) if basename == "pyproject.toml"
404
+ return File.dirname(path) if basename == "setup.py"
405
+
406
+ return path if path.end_with?(".tar.gz")
407
+ return path if path.end_with?(".tgz") # amqplib v1.0.2 does this
408
+ return path if path.end_with?(".whl")
409
+ return path if path.end_with?(".zip")
410
+ return path if File.exist?(File.join(path, "setup.py"))
411
+ return path if File.exist?(File.join(path, "pyproject.toml"))
412
+
413
+ raise [
414
+ "Local file doesn't appear to be a supported type for a python package. Expected one of:",
415
+ " - A directory containing setup.py or pyproject.toml",
416
+ " - A file ending in .tar.gz (a python source dist)",
417
+ " - A file ending in .whl (a python wheel)",
418
+ ].join("\n")
124
419
  end
125
420
 
126
421
  logger.info("Trying to download", :package => package)
@@ -134,24 +429,16 @@ class FPM::Package::Python < FPM::Package
134
429
  target = build_path(package)
135
430
  FileUtils.mkdir(target) unless File.directory?(target)
136
431
 
137
- if attributes[:python_internal_pip?]
138
- # XXX: Should we detect if internal pip is available?
139
- attributes[:python_pip] = [ attributes[:python_bin], "-m", "pip"]
140
- end
141
-
142
432
  # attributes[:python_pip] -- expected to be a path
143
433
  if attributes[:python_pip]
144
434
  logger.debug("using pip", :pip => attributes[:python_pip])
145
- # TODO: Support older versions of pip
146
-
147
435
  pip = [attributes[:python_pip]] if pip.is_a?(String)
148
436
  setup_cmd = [
149
437
  *attributes[:python_pip],
150
438
  "download",
151
439
  "--no-clean",
152
440
  "--no-deps",
153
- "--no-binary", ":all:",
154
- "-d", build_path,
441
+ "-d", target,
155
442
  "-i", attributes[:python_pypi],
156
443
  ]
157
444
 
@@ -166,130 +453,105 @@ class FPM::Package::Python < FPM::Package
166
453
 
167
454
  safesystem(*setup_cmd)
168
455
 
169
- # Pip removed the --build flag sometime in 2021, it seems: https://github.com/pypa/pip/issues/8333
170
- # A workaround for pip removing the `--build` flag. Previously, `pip download --build ...` would leave
171
- # behind a directory with the Python package extracted and ready to be used.
172
- # For example, `pip download ... Django` puts `Django-4.0.4.tar.tz` into the build_path directory.
173
- # If we expect `pip` to leave an unknown-named file in the `build_path` directory, let's check for
174
- # a single file and unpack it.
175
- files = ::Dir.glob(File.join(build_path, "*.{tar.gz,zip}"))
456
+ files = ::Dir.entries(target).filter { |entry| entry =~ /\.(whl|tgz|tar\.gz|zip)$/ }
176
457
  if files.length != 1
177
- raise "Unexpected directory layout after `pip download ...`. This might be an fpm bug? The directory is #{build_path}"
178
- end
179
-
180
- if files[0].end_with?("tar.gz")
181
- safesystem("tar", "-zxf", files[0], "-C", target)
182
- elsif files[0].end_with?("zip")
183
- safesystem("unzip", files[0], "-d", target)
184
- else
185
- raise "Unexpected file format after `pip download ...`. This might be an fpm bug? The file is #{files[0]}"
458
+ raise "Unexpected directory layout after `pip download ...`. This might be an fpm bug? The directory contains these files: #{files.inspect}"
186
459
  end
460
+ return File.join(target, files.first)
187
461
  else
188
462
  # no pip, use easy_install
189
463
  logger.debug("no pip, defaulting to easy_install", :easy_install => attributes[:python_easyinstall])
190
464
  safesystem(attributes[:python_easyinstall], "-i",
191
465
  attributes[:python_pypi], "--editable", "-U",
192
466
  "--build-directory", target, want_pkg)
467
+ # easy_install will put stuff in @tmpdir/packagename/, so find that:
468
+ # @tmpdir/somepackage/setup.py
469
+ #dirs = ::Dir.glob(File.join(target, "*"))
470
+ files = ::Dir.entries(target).filter { |entry| entry != "." && entry != ".." }
471
+ if dirs.length != 1
472
+ raise "Unexpected directory layout after easy_install. Maybe file a bug? The directory is #{build_path}"
473
+ end
474
+ return dirs.first
193
475
  end
194
-
195
- # easy_install will put stuff in @tmpdir/packagename/, so find that:
196
- # @tmpdir/somepackage/setup.py
197
- dirs = ::Dir.glob(File.join(target, "*"))
198
- if dirs.length != 1
199
- raise "Unexpected directory layout after easy_install. Maybe file a bug? The directory is #{build_path}"
200
- end
201
- return dirs.first
202
476
  end # def download
203
477
 
204
478
  # Load the package information like name, version, dependencies.
205
- def load_package_info(setup_py)
206
- if !attributes[:python_package_prefix].nil?
207
- attributes[:python_package_name_prefix] = attributes[:python_package_prefix]
208
- end
209
-
210
- begin
211
- json_test_code = [
212
- "try:",
213
- " import json",
214
- "except ImportError:",
215
- " import simplejson as json"
216
- ].join("\n")
217
- safesystem("#{attributes[:python_bin]} -c '#{json_test_code}'")
218
- rescue FPM::Util::ProcessFailed => e
219
- logger.error("Your python environment is missing json support (either json or simplejson python module). I cannot continue without this.", :python => attributes[:python_bin], :error => e)
220
- raise FPM::Util::ProcessFailed, "Python (#{attributes[:python_bin]}) is missing simplejson or json modules."
221
- end
479
+ def load_package_info(path)
480
+ if path.end_with?(".whl")
481
+ # XXX: Maybe use rubyzip to parse the .whl (zip) file instead?
482
+ metadata = nil
483
+ execmd(["unzip", "-p", path, "*.dist-info/METADATA"], :stdin => false, :stderr => false) do |stdout|
484
+ metadata = PythonMetadata.from(stdout.read(64<<10))
485
+ end
222
486
 
223
- begin
224
- safesystem("#{attributes[:python_bin]} -c 'import pkg_resources'")
225
- rescue FPM::Util::ProcessFailed => e
226
- logger.error("Your python environment is missing a working setuptools module. I tried to find the 'pkg_resources' module but failed.", :python => attributes[:python_bin], :error => e)
227
- raise FPM::Util::ProcessFailed, "Python (#{attributes[:python_bin]}) is missing pkg_resources module."
487
+ wheeldata = nil
488
+ execmd(["unzip", "-p", path, "*.dist-info/WHEEL"], :stdin => false, :stderr => false) do |stdout|
489
+ wheeldata, _ = PythonMetadata.parse(stdout.read(64<<10))
490
+ end
491
+ else
492
+ raise "Unexpected python package path. This might be an fpm bug? The path is #{path}"
228
493
  end
229
494
 
230
- # Add ./pyfpm/ to the python library path
231
- pylib = File.expand_path(File.dirname(__FILE__))
495
+ self.architecture = wheeldata["Root-Is-Purelib"] == "true" ? "all" : "native"
232
496
 
233
- # chdir to the directory holding setup.py because some python setup.py's assume that you are
234
- # in the same directory.
235
- setup_dir = File.dirname(setup_py)
497
+ self.description = metadata.description unless metadata.description.nil?
498
+ self.license = metadata.license unless metadata.license.nil?
499
+ self.version = metadata.version
500
+ self.url = metadata.homepage unless metadata.homepage.nil?
236
501
 
237
- output = ::Dir.chdir(setup_dir) do
238
- tmp = build_path("metadata.json")
239
- setup_cmd = "env PYTHONPATH=#{pylib.shellescape}:$PYTHONPATH #{attributes[:python_bin]} " \
240
- "setup.py --command-packages=pyfpm get_metadata --output=#{tmp}"
241
-
242
- if attributes[:python_obey_requirements_txt?]
243
- setup_cmd += " --load-requirements-txt"
244
- end
245
-
246
- # Capture the output, which will be JSON metadata describing this python
247
- # package. See fpm/lib/fpm/package/pyfpm/get_metadata.py for more
248
- # details.
249
- logger.info("fetching package metadata", :setup_cmd => setup_cmd)
250
-
251
- success = safesystem(setup_cmd)
252
- #%x{#{setup_cmd}}
253
- if !success
254
- logger.error("setup.py get_metadata failed", :command => setup_cmd,
255
- :exitcode => $?.exitstatus)
256
- raise "An unexpected error occurred while processing the setup.py file"
257
- end
258
- File.read(tmp)
259
- end
260
- logger.debug("result from `setup.py get_metadata`", :data => output)
261
- metadata = JSON.parse(output)
262
- logger.info("object output of get_metadata", :json => metadata)
263
-
264
- self.architecture = metadata["architecture"]
265
- self.description = metadata["description"]
266
- # Sometimes the license field is multiple lines; do best-effort and just
267
- # use the first line.
268
- if metadata["license"]
269
- self.license = metadata["license"].split(/[\r\n]+/).first
270
- end
271
- self.version = metadata["version"]
272
- self.url = metadata["url"]
502
+ self.name = metadata.name
273
503
 
274
504
  # name prefixing is optional, if enabled, a name 'foo' will become
275
505
  # 'python-foo' (depending on what the python_package_name_prefix is)
276
- if attributes[:python_fix_name?]
277
- self.name = fix_name(metadata["name"])
278
- else
279
- self.name = metadata["name"]
280
- end
506
+ self.name = fix_name(self.name) if attributes[:python_fix_name?]
281
507
 
282
508
  # convert python-Foo to python-foo if flag is set
283
509
  self.name = self.name.downcase if attributes[:python_downcase_name?]
284
510
 
511
+ self.maintainer = metadata.maintainer
512
+
285
513
  if !attributes[:no_auto_depends?] and attributes[:python_dependencies?]
286
- metadata["dependencies"].each do |dep|
287
- dep_re = /^([^<>!= ]+)\s*(?:([~<>!=]{1,2})\s*(.*))?$/
514
+ # Python Dependency specifiers are a somewhat complex format described here:
515
+ # https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers
516
+ #
517
+ # We can ask python's packaging module to parse and evaluate these.
518
+ # XXX: Allow users to override environnment values.
519
+ #
520
+ # Example:
521
+ # Requires-Dist: tzdata; sys_platform = win32
522
+ # Requires-Dist: asgiref>=3.8.1
523
+
524
+ dep_re = /^([^<>!= ]+)\s*(?:([~<>!=]{1,2})\s*(.*))?$/
525
+
526
+ reqs = []
527
+
528
+ # --python-obey-requirements-txt should use requirements.txt
529
+ # (if found in the python package) and replace the requirments listed from the metadata
530
+ if attributes[:python_obey_requirements_txt?] && !@requirements_txt.nil?
531
+ requires = @requirements_txt
532
+ else
533
+ requires = metadata.requires
534
+ end
535
+
536
+ # Evaluate python package requirements and only show ones matching the current environment
537
+ # (Environment markers, etc)
538
+ # Additionally, 'extra' features such as a requirement named `django[bcrypt]` isn't quite supported yet,
539
+ # since the marker.evaluate() needs to be passed some environment like { "extra": "bcrypt" }
540
+ execmd([attributes[:python_bin], File.expand_path(File.join("pyfpm", "parse_requires.py"), File.dirname(__FILE__))]) do |stdin, stdout, stderr|
541
+ requires.each { |r| stdin.puts(r) }
542
+ stdin.close
543
+ data = stdout.read
544
+ logger.pipe(stderr => :warn)
545
+ reqs += JSON.parse(data)
546
+ end
547
+
548
+ reqs.each do |dep|
288
549
  match = dep_re.match(dep)
289
550
  if match.nil?
290
551
  logger.error("Unable to parse dependency", :dependency => dep)
291
552
  raise FPM::InvalidPackageConfiguration, "Invalid dependency '#{dep}'"
292
553
  end
554
+
293
555
  name, cmp, version = match.captures
294
556
 
295
557
  next if attributes[:python_disable_dependency].include?(name)
@@ -308,8 +570,12 @@ class FPM::Package::Python < FPM::Package
308
570
  # convert dependencies from python-Foo to python-foo
309
571
  name = name.downcase if attributes[:python_downcase_dependencies?]
310
572
 
311
- self.dependencies << "#{name} #{cmp} #{version}"
312
- end
573
+ if cmp.nil? && version.nil?
574
+ self.dependencies << "#{name}"
575
+ else
576
+ self.dependencies << "#{name} #{cmp} #{version}"
577
+ end
578
+ end # parse Requires-Dist dependencies
313
579
  end # if attributes[:python_dependencies?]
314
580
  end # def load_package_info
315
581
 
@@ -329,55 +595,17 @@ class FPM::Package::Python < FPM::Package
329
595
  end # def fix_name
330
596
 
331
597
  # Install this package to the staging directory
332
- def install_to_staging(setup_py)
333
- project_dir = File.dirname(setup_py)
334
-
598
+ def install_to_staging(path)
335
599
  prefix = "/"
336
600
  prefix = attributes[:prefix] unless attributes[:prefix].nil?
337
601
 
338
- # Some setup.py's assume $PWD == current directory of setup.py, so let's
339
- # chdir first.
340
- ::Dir.chdir(project_dir) do
341
- flags = [ "--root", staging_path ]
342
- if !attributes[:python_install_lib].nil?
343
- flags += [ "--install-lib", File.join(prefix, attributes[:python_install_lib]) ]
344
- elsif !attributes[:prefix].nil?
345
- # setup.py install --prefix PREFIX still installs libs to
346
- # PREFIX/lib64/python2.7/site-packages/
347
- # but we really want something saner.
348
- #
349
- # since prefix is given, but not python_install_lib, assume PREFIX/lib
350
- flags += [ "--install-lib", File.join(prefix, "lib") ]
351
- end
352
-
353
- if !attributes[:python_install_data].nil?
354
- flags += [ "--install-data", File.join(prefix, attributes[:python_install_data]) ]
355
- elsif !attributes[:prefix].nil?
356
- # prefix given, but not python_install_data, assume PREFIX/data
357
- flags += [ "--install-data", File.join(prefix, "data") ]
358
- end
359
-
360
- if !attributes[:python_install_bin].nil?
361
- flags += [ "--install-scripts", File.join(prefix, attributes[:python_install_bin]) ]
362
- elsif !attributes[:prefix].nil?
363
- # prefix given, but not python_install_bin, assume PREFIX/bin
364
- flags += [ "--install-scripts", File.join(prefix, "bin") ]
365
- end
366
-
367
- if !attributes[:python_scripts_executable].nil?
368
- # Overwrite installed python scripts shebang binary with provided executable
369
- flags += [ "build_scripts", "--executable", attributes[:python_scripts_executable] ]
370
- end
602
+ # XXX: Note: pip doesn't seem to have any equivalent to `--install-lib` or similar flags.
603
+ # XXX: Deprecate :python_install_data, :python_install_lib, :python_install_bin
604
+ # XXX: Deprecate: :python_setup_py_arguments
605
+ flags = [ "--root", staging_path ]
606
+ flags += [ "--prefix", prefix ] if !attributes[:prefix].nil?
371
607
 
372
- if !attributes[:python_setup_py_arguments].nil? and !attributes[:python_setup_py_arguments].empty?
373
- # Add optional setup.py arguments
374
- attributes[:python_setup_py_arguments].each do |a|
375
- flags += [ a ]
376
- end
377
- end
378
-
379
- safesystem(attributes[:python_bin], "setup.py", "install", *flags)
380
- end
608
+ safesystem(*attributes[:python_pip], "install", "--no-deps", *flags, path)
381
609
  end # def install_to_staging
382
610
 
383
611
  public(:input)
@@ -156,7 +156,7 @@ class FPM::Package::RPM < FPM::Package
156
156
 
157
157
  option "--macro-expansion", :flag,
158
158
  "install-time macro expansion in %pre %post %preun %postun scripts " \
159
- "(see: https://rpm.org/user_doc/scriptlet_expansion.html)", :default => false
159
+ "(see: https://rpm-software-management.github.io/rpm/manual/scriptlet_expansion.html)", :default => false
160
160
 
161
161
  option "--verifyscript", "FILE",
162
162
  "a script to be run on verification" do |val|
@@ -175,7 +175,7 @@ class FPM::Package::RPM < FPM::Package
175
175
  rpm_trigger = []
176
176
  option "--trigger-#{trigger_type}", "'[OPT]PACKAGE: FILEPATH'", "Adds a rpm trigger script located in FILEPATH, " \
177
177
  "having 'OPT' options and linking to 'PACKAGE'. PACKAGE can be a comma seperated list of packages. " \
178
- "See: http://rpm.org/api/4.4.2.2/triggers.html" do |trigger|
178
+ "See: https://rpm-software-management.github.io/rpm/manual/triggers.html" do |trigger|
179
179
  match = trigger.match(/^(\[.*\]|)(.*): (.*)$/)
180
180
  @logger.fatal("Trigger '#{trigger_type}' definition can't be parsed ('#{trigger}')") unless match
181
181
  opt, pkg, file = match.captures
@@ -199,8 +199,8 @@ class FPM::Package::RPM < FPM::Package
199
199
  # If and only if any of the above are done, then also replace ' with \', " with \", and \ with \\\\
200
200
  # to accommodate escape and quote processing that rpm will perform in that case (but not otherwise)
201
201
  def rpm_fix_name(name)
202
- if name.match?(/[ \t*?%$\[\]]/)
203
- name = name.gsub(/(\ |\t|\[|\]|\*|\?|\%|\$|'|"|\\)/, {
202
+ if name.match?(/[ \t*?%${}\[\]]/)
203
+ name = name.gsub(/(\ |\t|\[|\]|\*|\?|\%|\$|'|"|\{|\}|\\)/, {
204
204
  ' ' => '?',
205
205
  "\t" => '?',
206
206
  '%' => '[%]',
@@ -209,6 +209,10 @@ class FPM::Package::RPM < FPM::Package
209
209
  '*' => '[*]',
210
210
  '[' => '[\[]',
211
211
  ']' => '[\]]',
212
+ #'{' => '[\{]',
213
+ #'}' => '[\}]',
214
+ '{' => '?',
215
+ '}' => '?',
212
216
  '"' => '\\"',
213
217
  "'" => "\\'",
214
218
  '\\' => '\\\\\\\\',
@@ -36,7 +36,7 @@ class FPM::Package::Virtualenv < FPM::Package
36
36
  :default => nil
37
37
 
38
38
  option "--setup-install", :flag, "After building virtualenv run setup.py install "\
39
- "useful when building a virtualenv for packages and including their requirements from "
39
+ "useful when building a virtualenv for packages and including their requirements from "\
40
40
  "requirements.txt"
41
41
 
42
42
  option "--system-site-packages", :flag, "Give the virtual environment access to the "\
@@ -158,6 +158,31 @@ class FPM::Package::Virtualenv < FPM::Package
158
158
  end
159
159
  end
160
160
 
161
+ # [2025-09-30] virtualenv-tools seems broken?
162
+ # The --update-path will look for a VIRTUAL_ENV= line in bin/activate,
163
+ # however, the version I tested looks for it with quotations, like VIRTUAL_ENV='
164
+ # And at time of writing, my `virtualenv` tool doesn't use quotations on this variable
165
+ #
166
+ # Maybe best case we can patch it here instead. The path update tool
167
+ # looks for the original virtualenv path and I think updates any bin
168
+ # files which point to it.
169
+ patched = []
170
+ activate_bin = File.join(virtualenv_build_folder, "bin/activate")
171
+ fd = File.open(activate_bin)
172
+ fd.each_line do |line|
173
+ re = /^VIRTUAL_ENV=([^'"].*)$/
174
+ match = line.match(re)
175
+ if match
176
+ # Quote the VIRTUAL_ENV var assignment to help virtualenv-tools work?
177
+ patched << "VIRTUAL_ENV='#{match}'\n"
178
+ else
179
+ patched << line
180
+ end
181
+ end
182
+ fd.close
183
+ File.write(activate_bin, patched.join)
184
+
185
+ # Rewrite the base path inside the virtualenv to prepare it to be packaged.
161
186
  ::Dir.chdir(virtualenv_build_folder) do
162
187
  safesystem("virtualenv-tools", "--update-path", virtualenv_folder)
163
188
  end
@@ -191,7 +216,6 @@ class FPM::Package::Virtualenv < FPM::Package
191
216
  dir.input(".")
192
217
  @staging_path = dir.staging_path
193
218
  dir.cleanup_build
194
-
195
219
  end # def input
196
220
 
197
221
  # Delete python precompiled files found in a given folder.
data/lib/fpm/package.rb CHANGED
@@ -3,7 +3,6 @@ require "fpm/util" # local
3
3
  require "pathname" # stdlib
4
4
  require "find"
5
5
  require "tmpdir" # stdlib
6
- require "ostruct"
7
6
  require "backports/latest"
8
7
  require "socket" # stdlib, for Socket.gethostname
9
8
  require "shellwords" # stdlib, for Shellwords.escape
data/lib/fpm/rake_task.rb CHANGED
@@ -1,13 +1,39 @@
1
1
  require "fpm/namespace"
2
- require "ostruct"
3
2
  require "rake"
4
3
  require "rake/tasklib"
5
4
 
6
5
  class FPM::RakeTask < Rake::TaskLib
6
+ class Options
7
+ attr_accessor :args
8
+
9
+ def initialize(defaults=nil)
10
+ if defaults.nil?
11
+ @h = Hash.new
12
+ else
13
+ @h = defaults
14
+ end
15
+ end
16
+
17
+ def method_missing(m, *args)
18
+ if m.end_with?("=")
19
+ raise ArgumentError, "#{self.class.name}##{m} ... Expected 1 arg, got #{args.length}" if args.length != 1
20
+ @h[m[0...-1]] = args[0]
21
+ else
22
+ raise ArgumentError, "Expected 0 arg, got #{args.length}" if args.length != 0
23
+ return @h[m]
24
+ end
25
+ end
26
+
27
+ def to_h
28
+ return @h
29
+ end
30
+ end # Options
31
+
7
32
  attr_reader :options
8
33
 
9
34
  def initialize(package_name, opts = {}, &block)
10
- @options = OpenStruct.new(:name => package_name.to_s)
35
+ #@options = OpenStruct.new(:name => package_name.to_s)
36
+ @options = Options.new(:name => package_name.to_s)
11
37
  @source, @target = opts.values_at(:source, :target).map(&:to_s)
12
38
  @directory = File.expand_path(opts[:directory].to_s)
13
39
 
@@ -18,8 +44,8 @@ class FPM::RakeTask < Rake::TaskLib
18
44
 
19
45
  task(options.name) do |_, task_args|
20
46
  block.call(*[options, task_args].first(block.arity)) if block_given?
21
- abort("Must specify args") unless options.respond_to?(:args)
22
- @args = options.delete_field(:args)
47
+ abort("Must specify args") if options.args.nil?
48
+ @args = options.args
23
49
  run_cli
24
50
  end
25
51
  end
data/lib/fpm/util.rb CHANGED
@@ -136,7 +136,7 @@ module FPM::Util
136
136
  raise ExecutableNotFound.new(program)
137
137
  end
138
138
 
139
- logger.debug("Running command", :args => args2)
139
+ logger.info("Running command", :args => args2)
140
140
 
141
141
  stdout_r, stdout_w = IO.pipe
142
142
  stderr_r, stderr_w = IO.pipe
@@ -332,7 +332,15 @@ module FPM::Util
332
332
 
333
333
 
334
334
  def copy_entry(src, dst, preserve=false, remove_destination=false)
335
- case File.ftype(src)
335
+ st = File.lstat(src)
336
+
337
+ filetype = if st.ftype == "file" && st.nlink > 1
338
+ "hardlink"
339
+ else
340
+ st.ftype
341
+ end
342
+
343
+ case filetype
336
344
  when 'fifo'
337
345
  if File.respond_to?(:mkfifo)
338
346
  File.mkfifo(dst)
@@ -350,18 +358,23 @@ module FPM::Util
350
358
  raise UnsupportedSpecialFile.new("File is device which fpm doesn't know how to copy (#{File.ftype(src)}): #{src}")
351
359
  when 'directory'
352
360
  FileUtils.mkdir(dst) unless File.exist? dst
353
- else
354
- # if the file with the same dev and inode has been copied already -
361
+ when 'hardlink'
362
+ # Handle hardlinks
363
+ # if the file with the same dev and inode has been copied already.
355
364
  # hard link it's copy to `dst`, otherwise make an actual copy
356
- st = File.lstat(src)
357
365
  known_entry = copied_entries[[st.dev, st.ino]]
358
366
  if known_entry
359
367
  FileUtils.ln(known_entry, dst)
368
+ logger.debug("Copying hardlink", :src => src, :dst => dst, :link => known_entry)
360
369
  else
361
370
  FileUtils.copy_entry(src, dst, preserve, false,
362
371
  remove_destination)
363
372
  copied_entries[[st.dev, st.ino]] = dst
364
373
  end
374
+ else
375
+ # Normal file, just copy it.
376
+ FileUtils.copy_entry(src, dst, preserve, false,
377
+ remove_destination)
365
378
  end # else...
366
379
  end # def copy_entry
367
380
 
data/lib/fpm/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module FPM
2
- VERSION = "1.16.0"
2
+ VERSION = "1.17.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fpm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.16.0
4
+ version: 1.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Sissel
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: cabin
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: 0.6.0
18
+ version: 0.9.1
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: 0.6.0
25
+ version: 0.9.1
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: backports
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -114,14 +113,14 @@ dependencies:
114
113
  requirements:
115
114
  - - "~>"
116
115
  - !ruby/object:Gem::Version
117
- version: 3.0.0
116
+ version: 3.13.0
118
117
  type: :development
119
118
  prerelease: false
120
119
  version_requirements: !ruby/object:Gem::Requirement
121
120
  requirements:
122
121
  - - "~>"
123
122
  - !ruby/object:Gem::Version
124
- version: 3.0.0
123
+ version: 3.13.0
125
124
  - !ruby/object:Gem::Dependency
126
125
  name: insist
127
126
  requirement: !ruby/object:Gem::Requirement
@@ -199,6 +198,7 @@ files:
199
198
  - lib/fpm/package/puppet.rb
200
199
  - lib/fpm/package/pyfpm/__init__.py
201
200
  - lib/fpm/package/pyfpm/get_metadata.py
201
+ - lib/fpm/package/pyfpm/parse_requires.py
202
202
  - lib/fpm/package/python.rb
203
203
  - lib/fpm/package/rpm.rb
204
204
  - lib/fpm/package/sh.rb
@@ -238,7 +238,6 @@ homepage: https://github.com/jordansissel/fpm
238
238
  licenses:
239
239
  - MIT-like
240
240
  metadata: {}
241
- post_install_message:
242
241
  rdoc_options: []
243
242
  require_paths:
244
243
  - lib
@@ -254,8 +253,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
254
253
  - !ruby/object:Gem::Version
255
254
  version: '0'
256
255
  requirements: []
257
- rubygems_version: 3.2.22
258
- signing_key:
256
+ rubygems_version: 3.6.7
259
257
  specification_version: 4
260
258
  summary: fpm - package building and mangling
261
259
  test_files: []