Compare commits
13 Commits
main
...
issue/use-
| Author | SHA1 | Date |
|---|---|---|
|
|
3754c6b487 | |
|
|
697e6ffa74 | |
|
|
48dc536d94 | |
|
|
ef5da20933 | |
|
|
ee9ce01204 | |
|
|
b78ff5a121 | |
|
|
a555dc7d90 | |
|
|
84b47e0417 | |
|
|
1b8f547930 | |
|
|
d6e1dbae1a | |
|
|
959ccf27ed | |
|
|
8a8ae741bf | |
|
|
e6ff904fd3 |
|
|
@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
|
||||||
|
|
||||||
# For logging
|
# For logging
|
||||||
# https://github.com/jordansissel/ruby-cabin
|
# https://github.com/jordansissel/ruby-cabin
|
||||||
spec.add_dependency("cabin", ">= 0.6.0") # license: Apache 2
|
spec.add_dependency("cabin", ">= 0.9.1") # license: Apache 2
|
||||||
|
|
||||||
# For backports to older rubies
|
# For backports to older rubies
|
||||||
# https://github.com/marcandre/backports
|
# https://github.com/marcandre/backports
|
||||||
|
|
@ -46,7 +46,7 @@ Gem::Specification.new do |spec|
|
||||||
# so I think this needs to be added explicitly?
|
# so I think this needs to be added explicitly?
|
||||||
spec.add_dependency("rexml")
|
spec.add_dependency("rexml")
|
||||||
|
|
||||||
spec.add_development_dependency("rspec", "~> 3.0.0") # license: MIT (according to wikipedia)
|
spec.add_development_dependency("rspec", "~> 3.13.0") # license: MIT (according to wikipedia)
|
||||||
spec.add_development_dependency("insist", "~> 1.0.0") # license: Apache 2
|
spec.add_development_dependency("insist", "~> 1.0.0") # license: Apache 2
|
||||||
spec.add_development_dependency("pry")
|
spec.add_development_dependency("pry")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
#import pkg_resources
|
||||||
|
import packaging.requirements
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Expect requirements lines via stdin.
|
||||||
|
#requirements = pkg_resources.parse_requirements(sys.stdin)
|
||||||
|
|
||||||
|
# Process environment markers, if any, and produce a list of requirements for the current environment.
|
||||||
|
def evaluate_requirements(fd):
|
||||||
|
all_requirements = [packaging.requirements.Requirement(line) for line in sys.stdin]
|
||||||
|
|
||||||
|
for req in all_requirements:
|
||||||
|
# XXX: Note: marker.evaluate() can be given a dict() containing environment values to overwrite
|
||||||
|
if req.marker is None or req.marker.evaluate():
|
||||||
|
if len(req.specifier) > 0:
|
||||||
|
for spec in req.specifier:
|
||||||
|
yield "%s%s" % (req.name, spec)
|
||||||
|
else:
|
||||||
|
yield str(req.name)
|
||||||
|
|
||||||
|
print(json.dumps(list(evaluate_requirements(sys.stdin))))
|
||||||
|
|
@ -51,76 +51,386 @@ class FPM::Package::Python < FPM::Package
|
||||||
option "--downcase-dependencies", :flag, "Should the package dependencies " \
|
option "--downcase-dependencies", :flag, "Should the package dependencies " \
|
||||||
"be in lowercase?", :default => true
|
"be in lowercase?", :default => true
|
||||||
|
|
||||||
option "--install-bin", "BIN_PATH", "The path to where python scripts " \
|
option "--install-bin", "BIN_PATH", "(DEPRECATED, does nothing) The path to where python scripts " \
|
||||||
"should be installed to."
|
"should be installed to." do
|
||||||
option "--install-lib", "LIB_PATH", "The path to where python libs " \
|
logger.warn("Using deprecated flag --install-bin")
|
||||||
|
end
|
||||||
|
option "--install-lib", "LIB_PATH", "(DEPRECATED, does nothing) The path to where python libs " \
|
||||||
"should be installed to (default depends on your python installation). " \
|
"should be installed to (default depends on your python installation). " \
|
||||||
"Want to find out what your target platform is using? Run this: " \
|
"Want to find out what your target platform is using? Run this: " \
|
||||||
"python -c 'from distutils.sysconfig import get_python_lib; " \
|
"python -c 'from distutils.sysconfig import get_python_lib; " \
|
||||||
"print get_python_lib()'"
|
"print get_python_lib()'" do
|
||||||
option "--install-data", "DATA_PATH", "The path to where data should be " \
|
logger.warn("Using deprecated flag --install-bin")
|
||||||
|
end
|
||||||
|
|
||||||
|
option "--install-data", "DATA_PATH", "(DEPRECATED, does nothing) The path to where data should be " \
|
||||||
"installed to. This is equivalent to 'python setup.py --install-data " \
|
"installed to. This is equivalent to 'python setup.py --install-data " \
|
||||||
"DATA_PATH"
|
"DATA_PATH" do
|
||||||
option "--dependencies", :flag, "Include requirements defined in setup.py" \
|
logger.warn("Using deprecated flag --install-bin")
|
||||||
|
end
|
||||||
|
|
||||||
|
option "--dependencies", :flag, "Include requirements defined by the python package" \
|
||||||
" as dependencies.", :default => true
|
" as dependencies.", :default => true
|
||||||
option "--obey-requirements-txt", :flag, "Use a requirements.txt file " \
|
option "--obey-requirements-txt", :flag, "Use a requirements.txt file " \
|
||||||
"in the top-level directory of the python package for dependency " \
|
"in the top-level directory of the python package for dependency " \
|
||||||
"detection.", :default => false
|
"detection.", :default => false
|
||||||
option "--scripts-executable", "PYTHON_EXECUTABLE", "Set custom python " \
|
option "--scripts-executable", "PYTHON_EXECUTABLE", "(DEPRECATED) Set custom python " \
|
||||||
"interpreter in installing scripts. By default distutils will replace " \
|
"interpreter in installing scripts. By default distutils will replace " \
|
||||||
"python interpreter in installing scripts (specified by shebang) with " \
|
"python interpreter in installing scripts (specified by shebang) with " \
|
||||||
"current python interpreter (sys.executable). This option is equivalent " \
|
"current python interpreter (sys.executable). This option is equivalent " \
|
||||||
"to appending 'build_scripts --executable PYTHON_EXECUTABLE' arguments " \
|
"to appending 'build_scripts --executable PYTHON_EXECUTABLE' arguments " \
|
||||||
"to 'setup.py install' command."
|
"to 'setup.py install' command." do
|
||||||
|
logger.warn("Using deprecated flag --install-bin")
|
||||||
|
end
|
||||||
|
|
||||||
option "--disable-dependency", "python_package_name",
|
option "--disable-dependency", "python_package_name",
|
||||||
"The python package name to remove from dependency list",
|
"The python package name to remove from dependency list",
|
||||||
:multivalued => true, :attribute_name => :python_disable_dependency,
|
:multivalued => true, :attribute_name => :python_disable_dependency,
|
||||||
:default => []
|
:default => []
|
||||||
option "--setup-py-arguments", "setup_py_argument",
|
option "--setup-py-arguments", "setup_py_argument",
|
||||||
"Arbitrary argument(s) to be passed to setup.py",
|
"(DEPRECATED) Arbitrary argument(s) to be passed to setup.py",
|
||||||
:multivalued => true, :attribute_name => :python_setup_py_arguments,
|
:multivalued => true, :attribute_name => :python_setup_py_arguments,
|
||||||
:default => []
|
:default => [] do
|
||||||
|
logger.warn("Using deprecated flag --install-bin")
|
||||||
|
end
|
||||||
option "--internal-pip", :flag,
|
option "--internal-pip", :flag,
|
||||||
"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",
|
"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",
|
||||||
:attribute_name => :python_internal_pip,
|
:attribute_name => :python_internal_pip,
|
||||||
:default => true
|
:default => true
|
||||||
|
|
||||||
|
# Environment markers which are known but not yet supported by fpm.
|
||||||
|
# For some of these markers, it's not even clear if they are useful to fpm's packaging step.
|
||||||
|
# https://packaging.python.org/en/latest/specifications/dependency-specifiers/#dependency-specifiers
|
||||||
|
#
|
||||||
|
# XXX: python's setuptools.pkg_resources can help parse and evaluate such things, even if that library is deprecated:
|
||||||
|
# >>> f = open("spec/fixtures/python/requirements.txt"); a = pkg_resources.parse_requirements(f.read()); f.close();
|
||||||
|
# >>> [x for x in list(a) if x.marker is None or x.marker.evaluate()]
|
||||||
|
# [Requirement.parse('rtxt-dep1>0.1'), Requirement.parse('rtxt-dep2==0.1'), Requirement.parse('rtxt-dep4; python_version > "2.0"')]
|
||||||
|
#
|
||||||
|
# Another example, showing only requirements which have environment markers which evaluate to true (or have no markers)
|
||||||
|
# python3 -c 'import pkg_resources; import json;import sys; r = pkg_resources.parse_requirements(sys.stdin); deps = [d for d in list(r) if d.marker is None or d.marker.evaluate()]; pr
|
||||||
|
# ["rtxt-dep1>0.1", "rtxt-dep2==0.1", "rtxt-dep4; python_version > \"2.0\""]
|
||||||
|
UNSUPPORTED_DEPENDENCY_MARKERS = %w(python_version python_full_version os_name platform_release platform_system platform_version
|
||||||
|
platform_machine platform_python_implementation implementation_name implementation_version)
|
||||||
|
|
||||||
private
|
|
||||||
|
class PythonMetadata
|
||||||
|
require "strscan"
|
||||||
|
|
||||||
|
class MissingField < StandardError; end
|
||||||
|
class UnexpectedContent < StandardError; end
|
||||||
|
|
||||||
|
# According to https://packaging.python.org/en/latest/specifications/core-metadata/
|
||||||
|
# > Core Metadata v2.4 - August 2024
|
||||||
|
MULTIPLE_USE = %w(Dynamic Platform Supported-Platform License-File Classifier Requires-Dist Requires-External Project-URL Provides-Extra Provides-Dist Obsoletes-Dist)
|
||||||
|
|
||||||
|
# METADATA files are described in Python Packaging "Core Metadata"[1] and appear to have roughly RFC822 syntax.
|
||||||
|
# [1] https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata
|
||||||
|
def self.parse(input)
|
||||||
|
s = StringScanner.new(input)
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
# Default "Multiple use" fields to empty array instead of nil.
|
||||||
|
MULTIPLE_USE.each do |field|
|
||||||
|
headers[field] = []
|
||||||
|
end
|
||||||
|
|
||||||
|
while !s.eos? and !s.scan("\n") do
|
||||||
|
# Field is non-space up, but excluding the colon
|
||||||
|
field = s.scan(/[^\s:]+/)
|
||||||
|
|
||||||
|
# Skip colon and following whitespace
|
||||||
|
s.scan(/:\s*/)
|
||||||
|
|
||||||
|
# Value is text until newline, and any following lines if they have leading spaces.
|
||||||
|
value = s.scan(/[^\n]+(?:\Z|\n(?:[ \t][^\n]+\n)*)/)
|
||||||
|
if value.nil?
|
||||||
|
raise "Failed parsing Python package metadata value at field #{field}, char offset #{s.pos}"
|
||||||
|
end
|
||||||
|
value = value.chomp
|
||||||
|
|
||||||
|
if MULTIPLE_USE.include?(field)
|
||||||
|
raise "Header field should be an array. This is a bug in fpm." if !headers[field].is_a?(Array)
|
||||||
|
headers[field] << value
|
||||||
|
else
|
||||||
|
headers[field] = value
|
||||||
|
end
|
||||||
|
end # while reading headers
|
||||||
|
|
||||||
|
# If there's more content beyond the last header, then it's a content body.
|
||||||
|
# In Python Metadata >= 2.1, the descriptino can be written in the body.
|
||||||
|
if !s.eos?
|
||||||
|
if headers["Metadata-Version"].to_f >= 2.1
|
||||||
|
# Per Python core-metadata spec:
|
||||||
|
# > Changed in version 2.1: This field may be specified in the message body instead.
|
||||||
|
#return PythonMetadata.new(headers, s.string[s.pos ...])
|
||||||
|
return headers, s.string[s.pos ... ]
|
||||||
|
elsif headers["Metadata-Version"].to_f >= 2.0
|
||||||
|
# dnspython v1.15.0 has a description body and Metadata-Version 2.0
|
||||||
|
# this seems out of spec, but let's accept it anyway.
|
||||||
|
return headers, s.string[s.pos ... ]
|
||||||
|
else
|
||||||
|
raise "After reading METADATA headers, extra data is in the file but was not expected. This may be a bug in fpm."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#return PythonMetadata.new(headers)
|
||||||
|
return headers, nil # nil means no body in this metadata
|
||||||
|
rescue => e
|
||||||
|
puts "String scan failed: #{e}"
|
||||||
|
puts "Position: #{s.pointer}"
|
||||||
|
puts "---"
|
||||||
|
puts input
|
||||||
|
puts "==="
|
||||||
|
puts input[s.pointer...]
|
||||||
|
puts "---"
|
||||||
|
raise e
|
||||||
|
end # self.parse
|
||||||
|
|
||||||
|
def self.from(input)
|
||||||
|
return PythonMetadata.new(*parse(input))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Only focusing on terms fpm may care about
|
||||||
|
attr_reader :name, :version, :summary, :description, :keywords, :maintainer, :license, :requires, :homepage
|
||||||
|
|
||||||
|
FIELD_MAP = {
|
||||||
|
:@name => "Name",
|
||||||
|
:@version => "Version",
|
||||||
|
:@summary => "Summary",
|
||||||
|
:@description => "Description",
|
||||||
|
:@keywords => "Keywords",
|
||||||
|
:@maintainer => "Author-email",
|
||||||
|
|
||||||
|
# Note: License can also come from the deprecated "License" field
|
||||||
|
# This is processed later in this method.
|
||||||
|
:@license => "License-Expression",
|
||||||
|
|
||||||
|
:@requires => "Requires-Dist",
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUIRED_FIELDS = [ "Metadata-Version", "Name", "Version" ]
|
||||||
|
|
||||||
|
# headers - a Hash containing field-value pairs from headers as read from a python METADATA file.
|
||||||
|
# body - optional, a string containing the body text of a METADATA file
|
||||||
|
def initialize(headers, body=nil)
|
||||||
|
REQUIRED_FIELDS.each do |field|
|
||||||
|
if !headers.include?(field)
|
||||||
|
raise MissingField, "Missing required Python metadata field, '#{field}'. This might be a bug in the package or in fpm."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
FIELD_MAP.each do |attr, field|
|
||||||
|
if headers.include?(field)
|
||||||
|
instance_variable_set(attr, headers.fetch(field))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Do any extra processing on fields to turn them into their expected content.
|
||||||
|
process_description(headers, body)
|
||||||
|
process_license(headers)
|
||||||
|
process_homepage(headers)
|
||||||
|
process_maintainer(headers)
|
||||||
|
end # def initialize
|
||||||
|
|
||||||
|
private
|
||||||
|
def process_description(headers, body)
|
||||||
|
if @description
|
||||||
|
# Per python core-metadata spec:
|
||||||
|
# > To support empty lines and lines with indentation with respect to the
|
||||||
|
# > RFC 822 format, any CRLF character has to be suffixed by 7 spaces
|
||||||
|
# > followed by a pipe (“|”) char. As a result, the Description field is
|
||||||
|
# > encoded into a folded field that can be interpreted by RFC822 parser [2].
|
||||||
|
@description = @description.gsub!(/^ |/, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
if !body.nil?
|
||||||
|
if headers["Metadata-Version"].to_f >= 2.1
|
||||||
|
# Per Python core-metadata spec:
|
||||||
|
# > Changed in version 2.1: [Description] field may be specified in the message body instead.
|
||||||
|
#
|
||||||
|
# The description is simply the rest of the METADATA file after the headers.
|
||||||
|
@description = body
|
||||||
|
elsif headers["Metadata-Version"].to_f >= 2.0
|
||||||
|
# dnspython v1.15.0 has a description body and Metadata-Version 2.0
|
||||||
|
# this seems out of spec, but let's accept it anyway.
|
||||||
|
@description = body
|
||||||
|
else
|
||||||
|
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."
|
||||||
|
end
|
||||||
|
|
||||||
|
# What to do if we find a description body but already have a Description field set in the headers?
|
||||||
|
if headers.include?("Description")
|
||||||
|
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."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# XXX: The description field can be markdown, plain text, or reST.
|
||||||
|
# Content type is noted in the "Description-Content-Type" field
|
||||||
|
# Should we transform this to plain text?
|
||||||
|
end # process_description
|
||||||
|
|
||||||
|
def process_license(headers)
|
||||||
|
# Ignore the "License" field if License-Expression is also present.
|
||||||
|
return if headers["Metadata-Version"].to_f >= 2.4 && headers.include?("License-Expression")
|
||||||
|
|
||||||
|
# Deprecated field, License, as described in python core-metadata:
|
||||||
|
# > As of Metadata 2.4, License and License-Expression are mutually exclusive.
|
||||||
|
# > If both are specified, tools which parse metadata will disregard License
|
||||||
|
# > and PyPI will reject uploads. See PEP 639.
|
||||||
|
if headers["License"]
|
||||||
|
# Note: This license can be free form text, so it's unclear if it's a great choice.
|
||||||
|
# however, the original python metadata License field is quite old/deprecated
|
||||||
|
# so maybe nobody uses it anymore?
|
||||||
|
@license = headers["License"]
|
||||||
|
elsif license_classifier = headers["Classifier"].find { |value| value =~ /^License ::/ }
|
||||||
|
# The license could also show up in the "Classifier" header with "License ::" as a prefix.
|
||||||
|
@license = license_classifier.sub(/^License ::/, "")
|
||||||
|
end # check for deprecated License field
|
||||||
|
end # process_license
|
||||||
|
|
||||||
|
def process_homepage(headers)
|
||||||
|
return if headers["Project-URL"].empty?
|
||||||
|
|
||||||
|
# Create a hash of Project-URL where the label is the key, url the value.
|
||||||
|
urls = Hash[*headers["Project-URL"].map do |text|
|
||||||
|
label, url = text.split(/, */, 2)
|
||||||
|
# Normalize the label by removing punctuation and spaces
|
||||||
|
# Reference: https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization
|
||||||
|
# > In plain language: a label is normalized by deleting all ASCII punctuation and whitespace, and then converting the result to lowercase.
|
||||||
|
label = label.gsub(/[[:punct:][:space:]]/, "").downcase
|
||||||
|
[label, url]
|
||||||
|
end.flatten(1)]
|
||||||
|
|
||||||
|
# Prioritize certain URL labels when choosing the homepage url.
|
||||||
|
[ "homepage", "source", "documentation", "releasenotes" ].each do |label|
|
||||||
|
if urls.include?(label)
|
||||||
|
@homepage = urls[label]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Otherwise, default to the first URL
|
||||||
|
@homepage = urls.values.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_maintainer(headers)
|
||||||
|
# Python metadata supports both "Author-email" and "Maintainer-email"
|
||||||
|
# Of the "Maintainer" fields, python core-metadata says:
|
||||||
|
# > Note that this field is intended for use when a project is being maintained by someone other than the original author
|
||||||
|
#
|
||||||
|
# So we should prefer Maintainer-email if it exists, but fall back to Author-email otherwise.
|
||||||
|
@maintainer = headers["Maintainer-email"] unless headers["Maintainer-email"].nil?
|
||||||
|
end
|
||||||
|
end # class PythonMetadata
|
||||||
|
|
||||||
# Input a package.
|
# Input a package.
|
||||||
#
|
#
|
||||||
# The 'package' can be any of:
|
# The 'package' can be any of:
|
||||||
#
|
#
|
||||||
# * A name of a package on pypi (ie; easy_install some-package)
|
# * A name of a package on pypi (ie; easy_install some-package)
|
||||||
# * The path to a directory containing setup.py
|
# * The path to a directory containing setup.py or pypackage.toml
|
||||||
# * The path to a setup.py
|
# * The path to a setup.py or pypackage.toml
|
||||||
|
# * The path to a python sdist file ending in .tar.gz
|
||||||
|
# * The path to a python wheel file ending in .whl
|
||||||
def input(package)
|
def input(package)
|
||||||
|
#if attributes[:python_obey_requirements_txt?]
|
||||||
|
#raise "--python-obey-requirements-txt is temporarily unsupported at this time."
|
||||||
|
#end
|
||||||
|
explore_environment
|
||||||
|
|
||||||
path_to_package = download_if_necessary(package, version)
|
path_to_package = download_if_necessary(package, version)
|
||||||
|
|
||||||
|
# Expect a setup.py or pypackage.toml if it's a directory.
|
||||||
if File.directory?(path_to_package)
|
if File.directory?(path_to_package)
|
||||||
setup_py = File.join(path_to_package, "setup.py")
|
if !(File.exist?(File.join(path_to_package, "setup.py")) or File.exist?(File.join(path_to_package, "pypackage.toml")))
|
||||||
else
|
logger.error("The path doesn't appear to be a python package directory. I expected either a pypackage.toml or setup.py but found neither.", :package => package)
|
||||||
setup_py = path_to_package
|
raise "Unable to find python package; tried #{setup_py}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if attributes[:python_obey_requirements_txt?] && File.exist?(File.join(path_to_package, "requirements.txt"))
|
||||||
|
@requirements_txt = File.read(File.join(path_to_package, "requirements.txt"))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if !File.exist?(setup_py)
|
if File.file?(path_to_package)
|
||||||
logger.error("Could not find 'setup.py'", :path => setup_py)
|
if ["setup.py", "pypackage.toml"].include?(File.basename(path_to_package))
|
||||||
raise "Unable to find python package; tried #{setup_py}"
|
path_to_package = File.dirname(path_to_package)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
load_package_info(setup_py)
|
if [".tar.gz", ".tgz"].any? { |suffix| path_to_package.end_with?(suffix) }
|
||||||
install_to_staging(setup_py)
|
# Have pip convert the .tar.gz (source dist?) into a wheel
|
||||||
|
logger.debug("Found tarball and assuming it's a python source package.")
|
||||||
|
safesystem(*attributes[:python_pip], "wheel", "--no-deps", "-w", build_path, path_to_package)
|
||||||
|
|
||||||
|
path_to_package = ::Dir.glob(build_path("*.whl")).first
|
||||||
|
if path_to_package.nil?
|
||||||
|
log.error("Failed building python package wheel format. This might be a bug in fpm.")
|
||||||
|
raise "Failed building python package format."
|
||||||
|
end
|
||||||
|
elsif File.directory?(path_to_package)
|
||||||
|
logger.debug("Found directory and assuming it's a python source package.")
|
||||||
|
safesystem(*attributes[:python_pip], "wheel", "--no-deps", "-w", build_path, path_to_package)
|
||||||
|
|
||||||
|
path_to_package = ::Dir.glob(build_path("*.whl")).first
|
||||||
|
if path_to_package.nil?
|
||||||
|
log.error("Failed building python package wheel format. This might be a bug in fpm.")
|
||||||
|
raise "Failed building python package format."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
load_package_info(path_to_package)
|
||||||
|
install_to_staging(path_to_package)
|
||||||
end # def input
|
end # def input
|
||||||
|
|
||||||
|
def explore_environment
|
||||||
|
if !attributes[:python_bin_given?]
|
||||||
|
# If --python-bin isn't set, try to find a good default python executable path, because it might not be "python"
|
||||||
|
pythons = [ "python", "python3", "python2" ]
|
||||||
|
default_python = pythons.find { |py| program_exists?(py) }
|
||||||
|
|
||||||
|
if default_python.nil?
|
||||||
|
raise FPM::Util::ExecutableNotFound, "Could not find any python interpreter. Tried the following: #{pythons.join(", ")}"
|
||||||
|
end
|
||||||
|
|
||||||
|
logger.info("Setting default python executable", :name => default_python)
|
||||||
|
attributes[:python_bin] = default_python
|
||||||
|
|
||||||
|
if !attributes[:python_package_name_prefix_given?]
|
||||||
|
attributes[:python_package_name_prefix] = default_python
|
||||||
|
logger.info("Setting package name prefix", :name => default_python)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if attributes[:python_internal_pip?]
|
||||||
|
# XXX: Should we detect if internal pip is available?
|
||||||
|
attributes[:python_pip] = [ attributes[:python_bin], "-m", "pip"]
|
||||||
|
end
|
||||||
|
end # explore_environment
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Download the given package if necessary. If version is given, that version
|
# Download the given package if necessary. If version is given, that version
|
||||||
# will be downloaded, otherwise the latest is fetched.
|
# will be downloaded, otherwise the latest is fetched.
|
||||||
def download_if_necessary(package, version=nil)
|
def download_if_necessary(package, version=nil)
|
||||||
# TODO(sissel): this should just be a 'download' method, the 'if_necessary'
|
|
||||||
# part should go elsewhere.
|
|
||||||
path = package
|
path = package
|
||||||
|
|
||||||
# If it's a path, assume local build.
|
# If it's a path, assume local build.
|
||||||
if File.directory?(path) or (File.exist?(path) and File.basename(path) == "setup.py")
|
if File.exist?(path)
|
||||||
return path
|
return path if File.directory?(path)
|
||||||
|
return path if path.end_with?(".tar.gz")
|
||||||
|
return path if path.end_with?(".tgz") # amqplib v1.0.2 does this
|
||||||
|
return path if path.end_with?(".whl")
|
||||||
|
return path if path.end_with?(".zip")
|
||||||
|
return path if File.exist?(File.join(path, "setup.py"))
|
||||||
|
return path if File.exist?(File.join(path, "pyproject.toml"))
|
||||||
|
|
||||||
|
raise [
|
||||||
|
"Local file doesn't appear to be a supported type for a python package. Expected one of:",
|
||||||
|
" - A directory containing setup.py or pyproject.toml",
|
||||||
|
" - A file ending in .tar.gz (a python source dist)",
|
||||||
|
" - A file ending in .whl (a python wheel)",
|
||||||
|
].join("\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
logger.info("Trying to download", :package => package)
|
logger.info("Trying to download", :package => package)
|
||||||
|
|
@ -134,24 +444,16 @@ class FPM::Package::Python < FPM::Package
|
||||||
target = build_path(package)
|
target = build_path(package)
|
||||||
FileUtils.mkdir(target) unless File.directory?(target)
|
FileUtils.mkdir(target) unless File.directory?(target)
|
||||||
|
|
||||||
if attributes[:python_internal_pip?]
|
|
||||||
# XXX: Should we detect if internal pip is available?
|
|
||||||
attributes[:python_pip] = [ attributes[:python_bin], "-m", "pip"]
|
|
||||||
end
|
|
||||||
|
|
||||||
# attributes[:python_pip] -- expected to be a path
|
# attributes[:python_pip] -- expected to be a path
|
||||||
if attributes[:python_pip]
|
if attributes[:python_pip]
|
||||||
logger.debug("using pip", :pip => attributes[:python_pip])
|
logger.debug("using pip", :pip => attributes[:python_pip])
|
||||||
# TODO: Support older versions of pip
|
|
||||||
|
|
||||||
pip = [attributes[:python_pip]] if pip.is_a?(String)
|
pip = [attributes[:python_pip]] if pip.is_a?(String)
|
||||||
setup_cmd = [
|
setup_cmd = [
|
||||||
*attributes[:python_pip],
|
*attributes[:python_pip],
|
||||||
"download",
|
"download",
|
||||||
"--no-clean",
|
"--no-clean",
|
||||||
"--no-deps",
|
"--no-deps",
|
||||||
"--no-binary", ":all:",
|
"-d", target,
|
||||||
"-d", build_path,
|
|
||||||
"-i", attributes[:python_pypi],
|
"-i", attributes[:python_pypi],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -166,130 +468,104 @@ class FPM::Package::Python < FPM::Package
|
||||||
|
|
||||||
safesystem(*setup_cmd)
|
safesystem(*setup_cmd)
|
||||||
|
|
||||||
# Pip removed the --build flag sometime in 2021, it seems: https://github.com/pypa/pip/issues/8333
|
files = ::Dir.entries(target).filter { |entry| entry =~ /\.(whl|tgz|tar\.gz|zip)$/ }
|
||||||
# A workaround for pip removing the `--build` flag. Previously, `pip download --build ...` would leave
|
|
||||||
# behind a directory with the Python package extracted and ready to be used.
|
|
||||||
# For example, `pip download ... Django` puts `Django-4.0.4.tar.tz` into the build_path directory.
|
|
||||||
# If we expect `pip` to leave an unknown-named file in the `build_path` directory, let's check for
|
|
||||||
# a single file and unpack it.
|
|
||||||
files = ::Dir.glob(File.join(build_path, "*.{tar.gz,zip}"))
|
|
||||||
if files.length != 1
|
if files.length != 1
|
||||||
raise "Unexpected directory layout after `pip download ...`. This might be an fpm bug? The directory is #{build_path}"
|
raise "Unexpected directory layout after `pip download ...`. This might be an fpm bug? The directory contains these files: #{files.inspect}"
|
||||||
end
|
|
||||||
|
|
||||||
if files[0].end_with?("tar.gz")
|
|
||||||
safesystem("tar", "-zxf", files[0], "-C", target)
|
|
||||||
elsif files[0].end_with?("zip")
|
|
||||||
safesystem("unzip", files[0], "-d", target)
|
|
||||||
else
|
|
||||||
raise "Unexpected file format after `pip download ...`. This might be an fpm bug? The file is #{files[0]}"
|
|
||||||
end
|
end
|
||||||
|
return File.join(target, files.first)
|
||||||
else
|
else
|
||||||
# no pip, use easy_install
|
# no pip, use easy_install
|
||||||
logger.debug("no pip, defaulting to easy_install", :easy_install => attributes[:python_easyinstall])
|
logger.debug("no pip, defaulting to easy_install", :easy_install => attributes[:python_easyinstall])
|
||||||
safesystem(attributes[:python_easyinstall], "-i",
|
safesystem(attributes[:python_easyinstall], "-i",
|
||||||
attributes[:python_pypi], "--editable", "-U",
|
attributes[:python_pypi], "--editable", "-U",
|
||||||
"--build-directory", target, want_pkg)
|
"--build-directory", target, want_pkg)
|
||||||
|
# easy_install will put stuff in @tmpdir/packagename/, so find that:
|
||||||
|
# @tmpdir/somepackage/setup.py
|
||||||
|
#dirs = ::Dir.glob(File.join(target, "*"))
|
||||||
|
files = ::Dir.entries(target).filter { |entry| entry != "." && entry != ".." }
|
||||||
|
if dirs.length != 1
|
||||||
|
raise "Unexpected directory layout after easy_install. Maybe file a bug? The directory is #{build_path}"
|
||||||
|
end
|
||||||
|
return dirs.first
|
||||||
end
|
end
|
||||||
|
|
||||||
# easy_install will put stuff in @tmpdir/packagename/, so find that:
|
|
||||||
# @tmpdir/somepackage/setup.py
|
|
||||||
dirs = ::Dir.glob(File.join(target, "*"))
|
|
||||||
if dirs.length != 1
|
|
||||||
raise "Unexpected directory layout after easy_install. Maybe file a bug? The directory is #{build_path}"
|
|
||||||
end
|
|
||||||
return dirs.first
|
|
||||||
end # def download
|
end # def download
|
||||||
|
|
||||||
# Load the package information like name, version, dependencies.
|
# Load the package information like name, version, dependencies.
|
||||||
def load_package_info(setup_py)
|
def load_package_info(path)
|
||||||
if !attributes[:python_package_prefix].nil?
|
if path.end_with?(".whl")
|
||||||
attributes[:python_package_name_prefix] = attributes[:python_package_prefix]
|
# XXX: Maybe use rubyzip to parse the .whl (zip) file instead?
|
||||||
end
|
metadata = nil
|
||||||
|
execmd(["unzip", "-p", path, "*.dist-info/METADATA"], :stdin => false, :stderr => false) do |stdout|
|
||||||
begin
|
metadata = PythonMetadata.from(stdout.read(64<<10))
|
||||||
json_test_code = [
|
|
||||||
"try:",
|
|
||||||
" import json",
|
|
||||||
"except ImportError:",
|
|
||||||
" import simplejson as json"
|
|
||||||
].join("\n")
|
|
||||||
safesystem("#{attributes[:python_bin]} -c '#{json_test_code}'")
|
|
||||||
rescue FPM::Util::ProcessFailed => e
|
|
||||||
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)
|
|
||||||
raise FPM::Util::ProcessFailed, "Python (#{attributes[:python_bin]}) is missing simplejson or json modules."
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
safesystem("#{attributes[:python_bin]} -c 'import pkg_resources'")
|
|
||||||
rescue FPM::Util::ProcessFailed => e
|
|
||||||
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)
|
|
||||||
raise FPM::Util::ProcessFailed, "Python (#{attributes[:python_bin]}) is missing pkg_resources module."
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add ./pyfpm/ to the python library path
|
|
||||||
pylib = File.expand_path(File.dirname(__FILE__))
|
|
||||||
|
|
||||||
# chdir to the directory holding setup.py because some python setup.py's assume that you are
|
|
||||||
# in the same directory.
|
|
||||||
setup_dir = File.dirname(setup_py)
|
|
||||||
|
|
||||||
output = ::Dir.chdir(setup_dir) do
|
|
||||||
tmp = build_path("metadata.json")
|
|
||||||
setup_cmd = "env PYTHONPATH=#{pylib.shellescape}:$PYTHONPATH #{attributes[:python_bin]} " \
|
|
||||||
"setup.py --command-packages=pyfpm get_metadata --output=#{tmp}"
|
|
||||||
|
|
||||||
if attributes[:python_obey_requirements_txt?]
|
|
||||||
setup_cmd += " --load-requirements-txt"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Capture the output, which will be JSON metadata describing this python
|
wheeldata = nil
|
||||||
# package. See fpm/lib/fpm/package/pyfpm/get_metadata.py for more
|
execmd(["unzip", "-p", path, "*.dist-info/WHEEL"], :stdin => false, :stderr => false) do |stdout|
|
||||||
# details.
|
wheeldata, _ = PythonMetadata.parse(stdout.read(64<<10))
|
||||||
logger.info("fetching package metadata", :setup_cmd => setup_cmd)
|
|
||||||
|
|
||||||
success = safesystem(setup_cmd)
|
|
||||||
#%x{#{setup_cmd}}
|
|
||||||
if !success
|
|
||||||
logger.error("setup.py get_metadata failed", :command => setup_cmd,
|
|
||||||
:exitcode => $?.exitstatus)
|
|
||||||
raise "An unexpected error occurred while processing the setup.py file"
|
|
||||||
end
|
end
|
||||||
File.read(tmp)
|
else
|
||||||
|
raise "Unexpected python package path. This might be an fpm bug? The path is #{path}"
|
||||||
end
|
end
|
||||||
logger.debug("result from `setup.py get_metadata`", :data => output)
|
|
||||||
metadata = JSON.parse(output)
|
|
||||||
logger.info("object output of get_metadata", :json => metadata)
|
|
||||||
|
|
||||||
self.architecture = metadata["architecture"]
|
self.architecture = wheeldata["Root-Is-Purelib"] == "true" ? "all" : "native"
|
||||||
self.description = metadata["description"]
|
|
||||||
# Sometimes the license field is multiple lines; do best-effort and just
|
self.description = metadata.description unless metadata.description.nil?
|
||||||
# use the first line.
|
self.license = metadata.license unless metadata.license.nil?
|
||||||
if metadata["license"]
|
self.version = metadata.version
|
||||||
self.license = metadata["license"].split(/[\r\n]+/).first
|
self.url = metadata.homepage unless metadata.homepage.nil?
|
||||||
end
|
|
||||||
self.version = metadata["version"]
|
self.name = metadata.name
|
||||||
self.url = metadata["url"]
|
|
||||||
|
|
||||||
# name prefixing is optional, if enabled, a name 'foo' will become
|
# name prefixing is optional, if enabled, a name 'foo' will become
|
||||||
# 'python-foo' (depending on what the python_package_name_prefix is)
|
# 'python-foo' (depending on what the python_package_name_prefix is)
|
||||||
if attributes[:python_fix_name?]
|
self.name = fix_name(self.name) if attributes[:python_fix_name?]
|
||||||
self.name = fix_name(metadata["name"])
|
|
||||||
else
|
|
||||||
self.name = metadata["name"]
|
|
||||||
end
|
|
||||||
|
|
||||||
# convert python-Foo to python-foo if flag is set
|
# convert python-Foo to python-foo if flag is set
|
||||||
self.name = self.name.downcase if attributes[:python_downcase_name?]
|
self.name = self.name.downcase if attributes[:python_downcase_name?]
|
||||||
|
|
||||||
|
self.maintainer = metadata.maintainer
|
||||||
|
|
||||||
if !attributes[:no_auto_depends?] and attributes[:python_dependencies?]
|
if !attributes[:no_auto_depends?] and attributes[:python_dependencies?]
|
||||||
metadata["dependencies"].each do |dep|
|
# Python Dependency specifiers are a somewhat complex format described here:
|
||||||
dep_re = /^([^<>!= ]+)\s*(?:([~<>!=]{1,2})\s*(.*))?$/
|
# https://packaging.python.org/en/latest/specifications/dependency-specifiers/#environment-markers
|
||||||
|
#
|
||||||
|
# We can ask python's packaging module to parse and evaluate these.
|
||||||
|
# XXX: Allow users to override environnment values.
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# Requires-Dist: tzdata; sys_platform = win32
|
||||||
|
# Requires-Dist: asgiref>=3.8.1
|
||||||
|
|
||||||
|
dep_re = /^([^<>!= ]+)\s*(?:([~<>!=]{1,2})\s*(.*))?$/
|
||||||
|
|
||||||
|
reqs = []
|
||||||
|
|
||||||
|
# --python-obey-requirements-txt should replace the requirments listed from the metadata
|
||||||
|
if attributes[:python_obey_requirements_txt?] && !@requirements_txt.nil?
|
||||||
|
requires = @requirements_txt.split("\n")
|
||||||
|
else
|
||||||
|
requires = metadata.requires
|
||||||
|
end
|
||||||
|
|
||||||
|
# Evaluate python package requirements and only show ones matching the current environment
|
||||||
|
# (Environment markers, etc)
|
||||||
|
# Additionally, 'extra' features such as a requirement named `django[bcrypt]` isn't quite supported yet,
|
||||||
|
# since the marker.evaluate() needs to be passed some environment like { "extra": "bcrypt" }
|
||||||
|
execmd([attributes[:python_bin], File.expand_path(File.join("pyfpm", "parse_requires.py"), File.dirname(__FILE__))]) do |stdin, stdout, stderr|
|
||||||
|
requires.each { |r| stdin.puts(r) }
|
||||||
|
stdin.close
|
||||||
|
data = stdout.read
|
||||||
|
logger.pipe(stderr => :warn)
|
||||||
|
reqs += JSON.parse(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
reqs.each do |dep|
|
||||||
match = dep_re.match(dep)
|
match = dep_re.match(dep)
|
||||||
if match.nil?
|
if match.nil?
|
||||||
logger.error("Unable to parse dependency", :dependency => dep)
|
logger.error("Unable to parse dependency", :dependency => dep)
|
||||||
raise FPM::InvalidPackageConfiguration, "Invalid dependency '#{dep}'"
|
raise FPM::InvalidPackageConfiguration, "Invalid dependency '#{dep}'"
|
||||||
end
|
end
|
||||||
|
|
||||||
name, cmp, version = match.captures
|
name, cmp, version = match.captures
|
||||||
|
|
||||||
next if attributes[:python_disable_dependency].include?(name)
|
next if attributes[:python_disable_dependency].include?(name)
|
||||||
|
|
@ -308,8 +584,12 @@ class FPM::Package::Python < FPM::Package
|
||||||
# convert dependencies from python-Foo to python-foo
|
# convert dependencies from python-Foo to python-foo
|
||||||
name = name.downcase if attributes[:python_downcase_dependencies?]
|
name = name.downcase if attributes[:python_downcase_dependencies?]
|
||||||
|
|
||||||
self.dependencies << "#{name} #{cmp} #{version}"
|
if cmp.nil? && version.nil?
|
||||||
end
|
self.dependencies << "#{name}"
|
||||||
|
else
|
||||||
|
self.dependencies << "#{name} #{cmp} #{version}"
|
||||||
|
end
|
||||||
|
end # parse Requires-Dist dependencies
|
||||||
end # if attributes[:python_dependencies?]
|
end # if attributes[:python_dependencies?]
|
||||||
end # def load_package_info
|
end # def load_package_info
|
||||||
|
|
||||||
|
|
@ -329,55 +609,17 @@ class FPM::Package::Python < FPM::Package
|
||||||
end # def fix_name
|
end # def fix_name
|
||||||
|
|
||||||
# Install this package to the staging directory
|
# Install this package to the staging directory
|
||||||
def install_to_staging(setup_py)
|
def install_to_staging(path)
|
||||||
project_dir = File.dirname(setup_py)
|
|
||||||
|
|
||||||
prefix = "/"
|
prefix = "/"
|
||||||
prefix = attributes[:prefix] unless attributes[:prefix].nil?
|
prefix = attributes[:prefix] unless attributes[:prefix].nil?
|
||||||
|
|
||||||
# Some setup.py's assume $PWD == current directory of setup.py, so let's
|
# XXX: Note: pip doesn't seem to have any equivalent to `--install-lib` or similar flags.
|
||||||
# chdir first.
|
# XXX: Deprecate :python_install_data, :python_install_lib, :python_install_bin
|
||||||
::Dir.chdir(project_dir) do
|
# XXX: Deprecate: :python_setup_py_arguments
|
||||||
flags = [ "--root", staging_path ]
|
flags = [ "--root", staging_path ]
|
||||||
if !attributes[:python_install_lib].nil?
|
flags += [ "--prefix", prefix ] if !attributes[:prefix].nil?
|
||||||
flags += [ "--install-lib", File.join(prefix, attributes[:python_install_lib]) ]
|
|
||||||
elsif !attributes[:prefix].nil?
|
|
||||||
# setup.py install --prefix PREFIX still installs libs to
|
|
||||||
# PREFIX/lib64/python2.7/site-packages/
|
|
||||||
# but we really want something saner.
|
|
||||||
#
|
|
||||||
# since prefix is given, but not python_install_lib, assume PREFIX/lib
|
|
||||||
flags += [ "--install-lib", File.join(prefix, "lib") ]
|
|
||||||
end
|
|
||||||
|
|
||||||
if !attributes[:python_install_data].nil?
|
safesystem(*attributes[:python_pip], "install", "--no-deps", *flags, path)
|
||||||
flags += [ "--install-data", File.join(prefix, attributes[:python_install_data]) ]
|
|
||||||
elsif !attributes[:prefix].nil?
|
|
||||||
# prefix given, but not python_install_data, assume PREFIX/data
|
|
||||||
flags += [ "--install-data", File.join(prefix, "data") ]
|
|
||||||
end
|
|
||||||
|
|
||||||
if !attributes[:python_install_bin].nil?
|
|
||||||
flags += [ "--install-scripts", File.join(prefix, attributes[:python_install_bin]) ]
|
|
||||||
elsif !attributes[:prefix].nil?
|
|
||||||
# prefix given, but not python_install_bin, assume PREFIX/bin
|
|
||||||
flags += [ "--install-scripts", File.join(prefix, "bin") ]
|
|
||||||
end
|
|
||||||
|
|
||||||
if !attributes[:python_scripts_executable].nil?
|
|
||||||
# Overwrite installed python scripts shebang binary with provided executable
|
|
||||||
flags += [ "build_scripts", "--executable", attributes[:python_scripts_executable] ]
|
|
||||||
end
|
|
||||||
|
|
||||||
if !attributes[:python_setup_py_arguments].nil? and !attributes[:python_setup_py_arguments].empty?
|
|
||||||
# Add optional setup.py arguments
|
|
||||||
attributes[:python_setup_py_arguments].each do |a|
|
|
||||||
flags += [ a ]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
safesystem(attributes[:python_bin], "setup.py", "install", *flags)
|
|
||||||
end
|
|
||||||
end # def install_to_staging
|
end # def install_to_staging
|
||||||
|
|
||||||
public(:input)
|
public(:input)
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ class FPM::Package::Virtualenv < FPM::Package
|
||||||
:default => nil
|
:default => nil
|
||||||
|
|
||||||
option "--setup-install", :flag, "After building virtualenv run setup.py install "\
|
option "--setup-install", :flag, "After building virtualenv run setup.py install "\
|
||||||
"useful when building a virtualenv for packages and including their requirements from "
|
"useful when building a virtualenv for packages and including their requirements from "\
|
||||||
"requirements.txt"
|
"requirements.txt"
|
||||||
|
|
||||||
option "--system-site-packages", :flag, "Give the virtual environment access to the "\
|
option "--system-site-packages", :flag, "Give the virtual environment access to the "\
|
||||||
|
|
@ -158,6 +158,31 @@ class FPM::Package::Virtualenv < FPM::Package
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# [2025-09-30] virtualenv-tools seems broken?
|
||||||
|
# The --update-path will look for a VIRTUAL_ENV= line in bin/activate,
|
||||||
|
# however, the version I tested looks for it with quotations, like VIRTUAL_ENV='
|
||||||
|
# And at time of writing, my `virtualenv` tool doesn't use quotations on this variable
|
||||||
|
#
|
||||||
|
# Maybe best case we can patch it here instead. The path update tool
|
||||||
|
# looks for the original virtualenv path and I think updates any bin
|
||||||
|
# files which point to it.
|
||||||
|
patched = []
|
||||||
|
activate_bin = File.join(virtualenv_build_folder, "bin/activate")
|
||||||
|
fd = File.open(activate_bin)
|
||||||
|
fd.each_line do |line|
|
||||||
|
re = /^VIRTUAL_ENV=([^'"].*)$/
|
||||||
|
match = line.match(re)
|
||||||
|
if match
|
||||||
|
# Quote the VIRTUAL_ENV var assignment to help virtualenv-tools work?
|
||||||
|
patched << "VIRTUAL_ENV='#{match}'\n"
|
||||||
|
else
|
||||||
|
patched << line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
fd.close
|
||||||
|
File.write(activate_bin, patched.join)
|
||||||
|
|
||||||
|
# Rewrite the base path inside the virtualenv to prepare it to be packaged.
|
||||||
::Dir.chdir(virtualenv_build_folder) do
|
::Dir.chdir(virtualenv_build_folder) do
|
||||||
safesystem("virtualenv-tools", "--update-path", virtualenv_folder)
|
safesystem("virtualenv-tools", "--update-path", virtualenv_folder)
|
||||||
end
|
end
|
||||||
|
|
@ -191,7 +216,6 @@ class FPM::Package::Virtualenv < FPM::Package
|
||||||
dir.input(".")
|
dir.input(".")
|
||||||
@staging_path = dir.staging_path
|
@staging_path = dir.staging_path
|
||||||
dir.cleanup_build
|
dir.cleanup_build
|
||||||
|
|
||||||
end # def input
|
end # def input
|
||||||
|
|
||||||
# Delete python precompiled files found in a given folder.
|
# Delete python precompiled files found in a given folder.
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ module FPM::Util
|
||||||
raise ExecutableNotFound.new(program)
|
raise ExecutableNotFound.new(program)
|
||||||
end
|
end
|
||||||
|
|
||||||
logger.debug("Running command", :args => args2)
|
logger.info("Running command", :args => args2)
|
||||||
|
|
||||||
stdout_r, stdout_w = IO.pipe
|
stdout_r, stdout_w = IO.pipe
|
||||||
stderr_r, stderr_w = IO.pipe
|
stderr_r, stderr_w = IO.pipe
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: Django
|
||||||
|
Version: 5.2.6
|
||||||
|
Summary: A high-level Python web framework that encourages rapid development and clean, pragmatic design.
|
||||||
|
Author-email: Django Software Foundation <foundation@djangoproject.com>
|
||||||
|
License: BSD-3-Clause
|
||||||
|
Project-URL: Homepage, https://www.djangoproject.com/
|
||||||
|
Project-URL: Documentation, https://docs.djangoproject.com/
|
||||||
|
Project-URL: Release notes, https://docs.djangoproject.com/en/stable/releases/
|
||||||
|
Project-URL: Funding, https://www.djangoproject.com/fundraising/
|
||||||
|
Project-URL: Source, https://github.com/django/django
|
||||||
|
Project-URL: Tracker, https://code.djangoproject.com/
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Environment :: Web Environment
|
||||||
|
Classifier: Framework :: Django
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3 :: Only
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||||
|
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
|
||||||
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
||||||
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||||
|
Requires-Python: >=3.10
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE
|
||||||
|
License-File: LICENSE.python
|
||||||
|
License-File: AUTHORS
|
||||||
|
Requires-Dist: asgiref>=3.8.1
|
||||||
|
Requires-Dist: sqlparse>=0.3.1
|
||||||
|
Requires-Dist: tzdata; sys_platform == "win32"
|
||||||
|
Provides-Extra: argon2
|
||||||
|
Requires-Dist: argon2-cffi>=19.1.0; extra == "argon2"
|
||||||
|
Provides-Extra: bcrypt
|
||||||
|
Requires-Dist: bcrypt; extra == "bcrypt"
|
||||||
|
Dynamic: license-file
|
||||||
|
|
||||||
|
======
|
||||||
|
Django
|
||||||
|
======
|
||||||
|
|
||||||
|
Django is a high-level Python web framework that encourages rapid development
|
||||||
|
and clean, pragmatic design. Thanks for checking it out.
|
||||||
|
|
||||||
|
All documentation is in the "``docs``" directory and online at
|
||||||
|
https://docs.djangoproject.com/en/stable/. If you're just getting started,
|
||||||
|
here's how we recommend you read the docs:
|
||||||
|
|
||||||
|
* First, read ``docs/intro/install.txt`` for instructions on installing Django.
|
||||||
|
|
||||||
|
* Next, work through the tutorials in order (``docs/intro/tutorial01.txt``,
|
||||||
|
``docs/intro/tutorial02.txt``, etc.).
|
||||||
|
|
||||||
|
* If you want to set up an actual deployment server, read
|
||||||
|
``docs/howto/deployment/index.txt`` for instructions.
|
||||||
|
|
||||||
|
* You'll probably want to read through the topical guides (in ``docs/topics``)
|
||||||
|
next; from there you can jump to the HOWTOs (in ``docs/howto``) for specific
|
||||||
|
problems, and check out the reference (``docs/ref``) for gory details.
|
||||||
|
|
||||||
|
* See ``docs/README`` for instructions on building an HTML version of the docs.
|
||||||
|
|
||||||
|
Docs are updated rigorously. If you find any problems in the docs, or think
|
||||||
|
they should be clarified in any way, please take 30 seconds to fill out a
|
||||||
|
ticket here: https://code.djangoproject.com/newticket
|
||||||
|
|
||||||
|
To get more help:
|
||||||
|
|
||||||
|
* Join the ``#django`` channel on ``irc.libera.chat``. Lots of helpful people
|
||||||
|
hang out there. `Webchat is available <https://web.libera.chat/#django>`_.
|
||||||
|
|
||||||
|
* Join the `Django Discord community <https://chat.djangoproject.com>`_.
|
||||||
|
|
||||||
|
* Join the community on the `Django Forum <https://forum.djangoproject.com/>`_.
|
||||||
|
|
||||||
|
To contribute to Django:
|
||||||
|
|
||||||
|
* Check out https://docs.djangoproject.com/en/dev/internals/contributing/ for
|
||||||
|
information about getting involved.
|
||||||
|
|
||||||
|
To run Django's test suite:
|
||||||
|
|
||||||
|
* Follow the instructions in the "Unit tests" section of
|
||||||
|
``docs/internals/contributing/writing-code/unit-tests.txt``, published online at
|
||||||
|
https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/#running-the-unit-tests
|
||||||
|
|
||||||
|
Supporting the Development of Django
|
||||||
|
====================================
|
||||||
|
|
||||||
|
Django's development depends on your contributions.
|
||||||
|
|
||||||
|
If you depend on Django, remember to support the Django Software Foundation: https://www.djangoproject.com/fundraising/
|
||||||
|
|
@ -10,9 +10,6 @@ setup(name="Example",
|
||||||
package_dir={},
|
package_dir={},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"Dependency1", "dependency2",
|
"Dependency1", "dependency2",
|
||||||
# XXX: I don't know what these python_version-dependent deps mean
|
|
||||||
# needs investigation
|
|
||||||
# Reference: PEP-0508
|
|
||||||
'rtxt-dep3; python_version == "2.0"',
|
'rtxt-dep3; python_version == "2.0"',
|
||||||
'rtxt-dep4; python_version > "2.0"',
|
'rtxt-dep4; python_version > "2.0"',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ end
|
||||||
describe FPM::Package::Python do
|
describe FPM::Package::Python do
|
||||||
before do
|
before do
|
||||||
skip("Python program not found") unless python_usable?
|
skip("Python program not found") unless python_usable?
|
||||||
subject.attributes[:python_bin] = find_python
|
#subject.attributes[:python_bin] = find_python
|
||||||
end
|
end
|
||||||
|
|
||||||
let (:example_dir) do
|
let (:example_dir) do
|
||||||
|
|
@ -50,16 +50,15 @@ describe FPM::Package::Python do
|
||||||
before :each do
|
before :each do
|
||||||
subject.attributes[:python_downcase_name?] = false
|
subject.attributes[:python_downcase_name?] = false
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when :python_fix_name? is true" do
|
context "when :python_fix_name? is true" do
|
||||||
before :each do
|
before :each do
|
||||||
subject.attributes[:python_fix_name?] = true
|
subject.attributes[:python_fix_name?] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
context "and :python_package_name_prefix is nil/default" do
|
context "and :python_package_name_prefix is nil/default" do
|
||||||
it "should prefix the package with 'python-'" do
|
it "should prefix the package name based on detected python-bin name" do
|
||||||
subject.input(example_dir)
|
subject.input(example_dir)
|
||||||
insist { subject.name } == "python-Example"
|
insist { subject.name } == "#{subject.attributes[:python_bin]}-Example"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -67,6 +66,7 @@ describe FPM::Package::Python do
|
||||||
it "should prefix the package name appropriately" do
|
it "should prefix the package name appropriately" do
|
||||||
prefix = "whoa"
|
prefix = "whoa"
|
||||||
subject.attributes[:python_package_name_prefix] = prefix
|
subject.attributes[:python_package_name_prefix] = prefix
|
||||||
|
subject.attributes[:python_package_name_prefix_given?] = true
|
||||||
subject.input(example_dir)
|
subject.input(example_dir)
|
||||||
insist { subject.name } == "#{prefix}-Example"
|
insist { subject.name } == "#{prefix}-Example"
|
||||||
end
|
end
|
||||||
|
|
@ -96,9 +96,10 @@ describe FPM::Package::Python do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "and :python_package_name_prefix is nil/default" do
|
context "and :python_package_name_prefix is nil/default" do
|
||||||
it "should prefix the package with 'python-'" do
|
it "should prefix the package based on the version of python" do
|
||||||
subject.input(example_dir)
|
subject.input(example_dir)
|
||||||
insist { subject.name } == "python-example"
|
insist { subject.attributes[:python_package_name_prefix_given?] }.nil?
|
||||||
|
insist { subject.name } == "#{subject.attributes[:python_bin]}-example"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -106,6 +107,7 @@ describe FPM::Package::Python do
|
||||||
it "should prefix the package name appropriately" do
|
it "should prefix the package name appropriately" do
|
||||||
prefix = "whoa"
|
prefix = "whoa"
|
||||||
subject.attributes[:python_package_name_prefix] = prefix
|
subject.attributes[:python_package_name_prefix] = prefix
|
||||||
|
subject.attributes[:python_package_name_prefix_given?] = true
|
||||||
subject.input(example_dir)
|
subject.input(example_dir)
|
||||||
insist { subject.name } == "#{prefix}-example"
|
insist { subject.name } == "#{prefix}-example"
|
||||||
end
|
end
|
||||||
|
|
@ -130,9 +132,24 @@ describe FPM::Package::Python do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "it should include the dependencies from setup.py" do
|
it "it should include the dependencies from setup.py" do
|
||||||
|
# Insist on using the defaults for this test, prefix not given and
|
||||||
|
# prefix should automatically be based on the python major version
|
||||||
|
insist { subject.attributes[:python_package_name_prefix_given?] }.nil?
|
||||||
subject.input(example_dir)
|
subject.input(example_dir)
|
||||||
|
|
||||||
|
prefix = subject.attributes[:python_package_name_prefix]
|
||||||
|
|
||||||
|
# The package name prefix attribute should be set to _something_ by default
|
||||||
|
reject { prefix }.nil?
|
||||||
|
|
||||||
# XXX: Why is there extra whitespace in these strings?
|
# XXX: Why is there extra whitespace in these strings?
|
||||||
insist { subject.dependencies.sort } == ["python-dependency1 ","python-dependency2 ", "python-rtxt-dep4 "]
|
#
|
||||||
|
# Note: The dependency list should only include entries which are supported by fpm.
|
||||||
|
# python dependencies can have 'environment markers' and most of those markers are
|
||||||
|
# not supported by fpm.
|
||||||
|
# In this test, there are (at time of writing) some python_version markers and fpm doesn't
|
||||||
|
# support those.
|
||||||
|
insist { subject.dependencies.sort } == ["#{prefix}-dependency1","#{prefix}-dependency2", "#{prefix}-rtxt-dep4"]
|
||||||
end
|
end
|
||||||
|
|
||||||
context "and :python_disable_dependency is set" do
|
context "and :python_disable_dependency is set" do
|
||||||
|
|
@ -142,7 +159,8 @@ describe FPM::Package::Python do
|
||||||
|
|
||||||
it "it should exclude the dependency" do
|
it "it should exclude the dependency" do
|
||||||
subject.input(example_dir)
|
subject.input(example_dir)
|
||||||
insist { subject.dependencies.sort } == ["python-dependency2 ", "python-rtxt-dep4 "]
|
prefix = subject.attributes[:python_package_name_prefix]
|
||||||
|
insist { subject.dependencies.sort } == ["#{prefix}-dependency2", "#{prefix}-rtxt-dep4"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -160,13 +178,15 @@ describe FPM::Package::Python do
|
||||||
|
|
||||||
it "it should prefix requirements.txt" do
|
it "it should prefix requirements.txt" do
|
||||||
subject.input(example_dir)
|
subject.input(example_dir)
|
||||||
insist { subject.dependencies.sort } == ["python-rtxt-dep1 > 0.1", "python-rtxt-dep2 = 0.1", "python-rtxt-dep4 "]
|
prefix = subject.attributes[:python_package_name_prefix]
|
||||||
|
insist { subject.dependencies.sort } == ["#{prefix}-rtxt-dep1 > 0.1", "#{prefix}-rtxt-dep2 = 0.1", "#{prefix}-rtxt-dep4"]
|
||||||
end
|
end
|
||||||
|
|
||||||
it "it should exclude the dependency" do
|
it "it should exclude the dependency" do
|
||||||
subject.attributes[:python_disable_dependency] = "rtxt-dep1"
|
subject.attributes[:python_disable_dependency] = "rtxt-dep1"
|
||||||
subject.input(example_dir)
|
subject.input(example_dir)
|
||||||
insist { subject.dependencies.sort } == ["python-rtxt-dep2 = 0.1", "python-rtxt-dep4 "]
|
prefix = subject.attributes[:python_package_name_prefix]
|
||||||
|
insist { subject.dependencies.sort } == ["#{prefix}-rtxt-dep2 = 0.1", "#{prefix}-rtxt-dep4"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -177,21 +197,20 @@ describe FPM::Package::Python do
|
||||||
|
|
||||||
it "it should load requirements.txt" do
|
it "it should load requirements.txt" do
|
||||||
subject.input(example_dir)
|
subject.input(example_dir)
|
||||||
insist { subject.dependencies.sort } == ["rtxt-dep1 > 0.1", "rtxt-dep2 = 0.1", "rtxt-dep4 "]
|
insist { subject.dependencies.sort } == ["rtxt-dep1 > 0.1", "rtxt-dep2 = 0.1", "rtxt-dep4"]
|
||||||
end
|
end
|
||||||
|
|
||||||
it "it should exclude the dependency" do
|
it "it should exclude the dependency" do
|
||||||
subject.attributes[:python_disable_dependency] = "rtxt-dep1"
|
subject.attributes[:python_disable_dependency] = "rtxt-dep1"
|
||||||
subject.input(example_dir)
|
subject.input(example_dir)
|
||||||
insist { subject.dependencies.sort } == ["rtxt-dep2 = 0.1", "rtxt-dep4 "]
|
insist { subject.dependencies.sort } == ["rtxt-dep2 = 0.1", "rtxt-dep4"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "python_scripts_executable is set" do
|
context "python_scripts_executable is set" do
|
||||||
it "should have scripts with a custom hashbang line" do
|
it "should have scripts with a custom hashbang line" do
|
||||||
pending("Disabled on travis-ci becaulamese it always fails, and there is no way to debug it?") if is_travis
|
skip("setup.py-specific feature is no longer supported")
|
||||||
skip("Requires python3 executable") unless program_exists?("python3")
|
|
||||||
|
|
||||||
subject.attributes[:python_scripts_executable] = "fancypants"
|
subject.attributes[:python_scripts_executable] = "fancypants"
|
||||||
# Newer versions of Django require Python 3.
|
# Newer versions of Django require Python 3.
|
||||||
|
|
@ -214,4 +233,85 @@ describe FPM::Package::Python do
|
||||||
insist { topline.chomp } == "#!fancypants"
|
insist { topline.chomp } == "#!fancypants"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when input is a name" do
|
||||||
|
it "should download from pypi" do
|
||||||
|
subject.input("click==8.3.0")
|
||||||
|
prefix = subject.attributes[:python_package_name_prefix]
|
||||||
|
|
||||||
|
insist { subject.name } == "#{prefix}-click"
|
||||||
|
insist { subject.version } == "8.3.0"
|
||||||
|
insist { subject.maintainer } == "Pallets <contact@palletsprojects.com>"
|
||||||
|
insist { subject.architecture } == "all"
|
||||||
|
insist { subject.dependencies } == [ ]
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
end # describe FPM::Package::Python
|
end # describe FPM::Package::Python
|
||||||
|
|
||||||
|
describe FPM::Package::Python::PythonMetadata do
|
||||||
|
|
||||||
|
context "processing simple examples" do
|
||||||
|
let(:text) {
|
||||||
|
[
|
||||||
|
"Metadata-Version: 2.4",
|
||||||
|
"Name: hello",
|
||||||
|
"Version: 1.0",
|
||||||
|
].join("\n") + "\n"
|
||||||
|
}
|
||||||
|
subject { described_class.from(text) }
|
||||||
|
|
||||||
|
it "should" do
|
||||||
|
insist { subject.name } == "hello"
|
||||||
|
insist { subject.version } == "1.0"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Use a known METADATA file from a real Python package
|
||||||
|
context "when parsing Django 5.2.6's METADATA" do
|
||||||
|
let(:text) do
|
||||||
|
File.read(File.expand_path("../../fixtures/python/METADATA", File.dirname(__FILE__)))
|
||||||
|
end
|
||||||
|
|
||||||
|
expectations = {
|
||||||
|
"Metadata-Version" => "2.4",
|
||||||
|
"Name" => "Django",
|
||||||
|
"Version" => "5.2.6",
|
||||||
|
"Summary" => "A high-level Python web framework that encourages rapid development and clean, pragmatic design.",
|
||||||
|
"Author-email" => "Django Software Foundation <foundation@djangoproject.com>",
|
||||||
|
"License" => "BSD-3-Clause",
|
||||||
|
}
|
||||||
|
|
||||||
|
let(:parsed) { described_class.parse(text) }
|
||||||
|
let(:headers) { parsed[0] }
|
||||||
|
let(:body) { parsed[1] }
|
||||||
|
|
||||||
|
let(:metadata) { described_class.from(text) }
|
||||||
|
|
||||||
|
expectations.each do |field, value|
|
||||||
|
it "the #{field} field should be #{value.inspect}" do
|
||||||
|
insist { headers[field] } == value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should parse multivalue fields into an array value" do
|
||||||
|
insist { headers["Classifier"] }.is_a?(Enumerable)
|
||||||
|
insist { headers["Project-URL"] }.is_a?(Enumerable)
|
||||||
|
insist { headers["Requires-Dist"] }.is_a?(Enumerable)
|
||||||
|
|
||||||
|
insist { headers["Requires-Dist"] }.include?('asgiref>=3.8.1')
|
||||||
|
insist { headers["Requires-Dist"] }.include?('sqlparse>=0.3.1')
|
||||||
|
insist { headers["Requires-Dist"] }.include?('tzdata; sys_platform == "win32"')
|
||||||
|
insist { headers["Requires-Dist"] }.include?('argon2-cffi>=19.1.0; extra == "argon2"')
|
||||||
|
insist { headers["Requires-Dist"] }.include?('bcrypt; extra == "bcrypt"')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should provide correctly parsed values" do
|
||||||
|
insist { metadata.name } == "Django"
|
||||||
|
insist { metadata.version } == "5.2.6"
|
||||||
|
insist { metadata.summary } == "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
|
||||||
|
insist { metadata.license } == "BSD-3-Clause"
|
||||||
|
insist { metadata.homepage } == "https://www.djangoproject.com/"
|
||||||
|
end
|
||||||
|
end # parsing Django METADATA
|
||||||
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue