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 +4 -4
- data/CHANGELOG.rst +12 -0
- data/lib/fpm/command.rb +4 -1
- data/lib/fpm/package/deb.rb +1 -0
- data/lib/fpm/package/dir.rb +0 -4
- data/lib/fpm/package/pacman.rb +8 -4
- data/lib/fpm/package/pyfpm/parse_requires.py +24 -0
- data/lib/fpm/package/python.rb +404 -176
- data/lib/fpm/package/rpm.rb +8 -4
- data/lib/fpm/package/virtualenv.rb +26 -2
- data/lib/fpm/package.rb +0 -1
- data/lib/fpm/rake_task.rb +30 -4
- data/lib/fpm/util.rb +18 -5
- data/lib/fpm/version.rb +1 -1
- metadata +8 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef536c2af546fea798392b6cca4c07b81b5f9a3d52654e72c5cb9543d1d4a21d
|
4
|
+
data.tar.gz: 95a4ea389bb037d2555ea95fd99313897b0f30c5b3bb8c5519a19d330b1e2842
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
data/lib/fpm/package/deb.rb
CHANGED
@@ -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
|
data/lib/fpm/package/dir.rb
CHANGED
@@ -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
|
data/lib/fpm/package/pacman.rb
CHANGED
@@ -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
|
41
|
-
when "amd64"
|
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
|
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))))
|
data/lib/fpm/package/python.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
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
|
107
|
-
|
108
|
-
|
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
|
-
|
112
|
-
|
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.
|
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
|
-
"
|
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
|
-
|
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
|
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(
|
206
|
-
if
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
231
|
-
pylib = File.expand_path(File.dirname(__FILE__))
|
495
|
+
self.architecture = wheeldata["Root-Is-Purelib"] == "true" ? "all" : "native"
|
232
496
|
|
233
|
-
|
234
|
-
|
235
|
-
|
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
|
-
|
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
|
-
|
287
|
-
|
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
|
-
|
312
|
-
|
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(
|
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
|
-
#
|
339
|
-
#
|
340
|
-
|
341
|
-
|
342
|
-
|
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
|
-
|
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)
|
data/lib/fpm/package/rpm.rb
CHANGED
@@ -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.
|
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:
|
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
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
|
-
|
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")
|
22
|
-
@args = options.
|
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.
|
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
|
-
|
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
|
-
|
354
|
-
#
|
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
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.
|
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:
|
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.
|
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.
|
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.
|
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.
|
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.
|
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: []
|