fpm-itchio 1.4.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 +7 -0
- data/CHANGELIST +629 -0
- data/CONTRIBUTORS +26 -0
- data/LICENSE +21 -0
- data/bin/fpm +8 -0
- data/lib/fpm.rb +18 -0
- data/lib/fpm/command.rb +642 -0
- data/lib/fpm/errors.rb +4 -0
- data/lib/fpm/namespace.rb +4 -0
- data/lib/fpm/package.rb +524 -0
- data/lib/fpm/package/cpan.rb +378 -0
- data/lib/fpm/package/deb.rb +887 -0
- data/lib/fpm/package/dir.rb +207 -0
- data/lib/fpm/package/empty.rb +13 -0
- data/lib/fpm/package/gem.rb +224 -0
- data/lib/fpm/package/npm.rb +120 -0
- data/lib/fpm/package/osxpkg.rb +164 -0
- data/lib/fpm/package/p5p.rb +124 -0
- data/lib/fpm/package/pacman.rb +397 -0
- data/lib/fpm/package/pear.rb +117 -0
- data/lib/fpm/package/pkgin.rb +35 -0
- data/lib/fpm/package/puppet.rb +120 -0
- data/lib/fpm/package/pyfpm/__init__.py +1 -0
- data/lib/fpm/package/pyfpm/get_metadata.py +104 -0
- data/lib/fpm/package/python.rb +317 -0
- data/lib/fpm/package/rpm.rb +583 -0
- data/lib/fpm/package/sh.rb +69 -0
- data/lib/fpm/package/solaris.rb +95 -0
- data/lib/fpm/package/tar.rb +74 -0
- data/lib/fpm/package/virtualenv.rb +145 -0
- data/lib/fpm/package/zip.rb +63 -0
- data/lib/fpm/rake_task.rb +59 -0
- data/lib/fpm/util.rb +253 -0
- data/lib/fpm/version.rb +3 -0
- data/templates/deb.erb +52 -0
- data/templates/deb/changelog.erb +5 -0
- data/templates/deb/ldconfig.sh.erb +13 -0
- data/templates/deb/postinst_upgrade.sh.erb +62 -0
- data/templates/deb/postrm_upgrade.sh.erb +46 -0
- data/templates/deb/preinst_upgrade.sh.erb +41 -0
- data/templates/deb/prerm_upgrade.sh.erb +39 -0
- data/templates/osxpkg.erb +11 -0
- data/templates/p5p_metadata.erb +12 -0
- data/templates/pacman.erb +47 -0
- data/templates/pacman/INSTALL.erb +41 -0
- data/templates/puppet/package.pp.erb +34 -0
- data/templates/puppet/package/remove.pp.erb +13 -0
- data/templates/rpm.erb +261 -0
- data/templates/rpm/filesystem_list +14514 -0
- data/templates/sh.erb +367 -0
- data/templates/solaris.erb +15 -0
- metadata +265 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
require "fpm/namespace"
|
2
|
+
require "fpm/package"
|
3
|
+
require "fileutils"
|
4
|
+
require "fpm/util"
|
5
|
+
|
6
|
+
# This provides PHP PEAR package support.
|
7
|
+
#
|
8
|
+
# This provides input support, but not output support.
|
9
|
+
class FPM::Package::PEAR < FPM::Package
|
10
|
+
option "--package-name-prefix", "PREFIX",
|
11
|
+
"Name prefix for pear package", :default => "php-pear"
|
12
|
+
|
13
|
+
option "--channel", "CHANNEL_URL",
|
14
|
+
"The pear channel url to use instead of the default."
|
15
|
+
|
16
|
+
option "--channel-update", :flag,
|
17
|
+
"call 'pear channel-update' prior to installation"
|
18
|
+
|
19
|
+
option "--bin-dir", "BIN_DIR",
|
20
|
+
"Directory to put binaries in"
|
21
|
+
|
22
|
+
option "--php-bin", "PHP_BIN",
|
23
|
+
"Specify php executable path if differs from the os used for packaging"
|
24
|
+
|
25
|
+
option "--php-dir", "PHP_DIR",
|
26
|
+
"Specify php dir relative to prefix if differs from pear default (pear/php)"
|
27
|
+
|
28
|
+
option "--data-dir", "DATA_DIR",
|
29
|
+
"Specify php dir relative to prefix if differs from pear default (pear/data)"
|
30
|
+
|
31
|
+
# Input a PEAR package.
|
32
|
+
#
|
33
|
+
# The parameter is a PHP PEAR package name.
|
34
|
+
#
|
35
|
+
# Attributes that affect behavior here:
|
36
|
+
# * :prefix - changes the install root, default is /usr/share
|
37
|
+
# * :pear_package_name_prefix - changes the
|
38
|
+
def input(input_package)
|
39
|
+
if !program_in_path?("pear")
|
40
|
+
raise ExecutableNotFound.new("pear")
|
41
|
+
end
|
42
|
+
|
43
|
+
# Create a temporary config file
|
44
|
+
logger.debug("Creating pear config file")
|
45
|
+
config = File.expand_path(build_path("pear.config"))
|
46
|
+
installroot = attributes[:prefix] || "/usr/share"
|
47
|
+
safesystem("pear", "config-create", staging_path(installroot), config)
|
48
|
+
|
49
|
+
if attributes[:pear_php_dir]
|
50
|
+
logger.info("Setting php_dir", :php_dir => attributes[:pear_php_dir])
|
51
|
+
safesystem("pear", "-c", config, "config-set", "php_dir", "#{staging_path(installroot)}/#{attributes[:pear_php_dir]}")
|
52
|
+
end
|
53
|
+
|
54
|
+
if attributes[:pear_data_dir]
|
55
|
+
logger.info("Setting data_dir", :data_dir => attributes[:pear_data_dir])
|
56
|
+
safesystem("pear", "-c", config, "config-set", "data_dir", "#{staging_path(installroot)}/#{attributes[:pear_data_dir]}")
|
57
|
+
end
|
58
|
+
|
59
|
+
bin_dir = attributes[:pear_bin_dir] || "usr/bin"
|
60
|
+
logger.info("Setting bin_dir", :bin_dir => bin_dir)
|
61
|
+
safesystem("pear", "-c", config, "config-set", "bin_dir", bin_dir)
|
62
|
+
|
63
|
+
php_bin = attributes[:pear_php_bin] || "/usr/bin/php"
|
64
|
+
logger.info("Setting php_bin", :php_bin => php_bin)
|
65
|
+
safesystem("pear", "-c", config, "config-set", "php_bin", php_bin)
|
66
|
+
|
67
|
+
# do channel-discover if required
|
68
|
+
if !attributes[:pear_channel].nil?
|
69
|
+
logger.info("Custom channel specified", :channel => attributes[:pear_channel])
|
70
|
+
channel_list = safesystemout("pear", "-c", config, "list-channels")
|
71
|
+
if channel_list !~ /#{Regexp.quote(attributes[:pear_channel])}/
|
72
|
+
logger.info("Discovering new channel", :channel => attributes[:pear_channel])
|
73
|
+
safesystem("pear", "-c", config, "channel-discover", attributes[:pear_channel])
|
74
|
+
end
|
75
|
+
input_package = "#{attributes[:pear_channel]}/#{input_package}"
|
76
|
+
logger.info("Prefixing package name with channel", :package => input_package)
|
77
|
+
end
|
78
|
+
|
79
|
+
# do channel-update if requested
|
80
|
+
if attributes[:pear_channel_update?]
|
81
|
+
channel = attributes[:pear_channel] || "pear"
|
82
|
+
logger.info("Updating the channel", :channel => channel)
|
83
|
+
safesystem("pear", "-c", config, "channel-update", channel)
|
84
|
+
end
|
85
|
+
|
86
|
+
logger.info("Installing pear package", :package => input_package,
|
87
|
+
:directory => staging_path)
|
88
|
+
::Dir.chdir(staging_path) do
|
89
|
+
safesystem("pear", "-c", config, "install", "-n", "-f", input_package)
|
90
|
+
end
|
91
|
+
|
92
|
+
pear_cmd = "pear -c #{config} remote-info #{input_package}"
|
93
|
+
logger.info("Fetching package information", :package => input_package, :command => pear_cmd)
|
94
|
+
name = %x{#{pear_cmd} | sed -ne '/^Package\s*/s/^Package\s*//p'}.chomp
|
95
|
+
self.name = "#{attributes[:pear_package_name_prefix]}-#{name}"
|
96
|
+
self.version = %x{#{pear_cmd} | sed -ne '/^Installed\s*/s/^Installed\s*//p'}.chomp
|
97
|
+
self.description = %x{#{pear_cmd} | sed -ne '/^Summary\s*/s/^Summary\s*//p'}.chomp
|
98
|
+
logger.debug("Package info", :name => self.name, :version => self.version,
|
99
|
+
:description => self.description)
|
100
|
+
|
101
|
+
# Remove the stuff we don't want
|
102
|
+
delete_these = [".depdb", ".depdblock", ".filemap", ".lock", ".channel", "cache", "temp", "download", ".channels", ".registry"]
|
103
|
+
Find.find(staging_path) do |path|
|
104
|
+
if File.file?(path)
|
105
|
+
logger.info("replacing staging_path in file", :replace_in => path, :staging_path => staging_path)
|
106
|
+
begin
|
107
|
+
content = File.read(path).gsub(/#{Regexp.escape(staging_path)}/, "")
|
108
|
+
File.write(path, content)
|
109
|
+
rescue ArgumentError => e
|
110
|
+
logger.warn("error replacing staging_path in file", :replace_in => path, :error => e)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
FileUtils.rm_r(path) if delete_these.include?(File.basename(path))
|
114
|
+
end
|
115
|
+
|
116
|
+
end # def input
|
117
|
+
end # class FPM::Package::PEAR
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class FPM::Package::Pkgin < FPM::Package
|
2
|
+
|
3
|
+
def output(output_path)
|
4
|
+
output_check(output_path)
|
5
|
+
|
6
|
+
File.write(build_path("build-info"), `pkg_info -X pkg_install | egrep '^(MACHINE_ARCH|OPSYS|OS_VERSION|PKGTOOLS_VERSION)'`)
|
7
|
+
|
8
|
+
cwd = ::Dir.pwd
|
9
|
+
::Dir.chdir(staging_path)
|
10
|
+
|
11
|
+
files = []
|
12
|
+
Find.find(".") do |path|
|
13
|
+
stat = File.lstat(path)
|
14
|
+
next unless stat.symlink? or stat.file?
|
15
|
+
files << path
|
16
|
+
end
|
17
|
+
::Dir.chdir(cwd)
|
18
|
+
|
19
|
+
File.write(build_path("packlist"), files.sort.join("\n"))
|
20
|
+
|
21
|
+
File.write(build_path("comment"), self.description + "\n")
|
22
|
+
|
23
|
+
File.write(build_path("description"), self.description + "\n")
|
24
|
+
|
25
|
+
args = [ "-B", build_path("build-info"), "-c", build_path("comment"), "-d", build_path("description"), "-f", build_path("packlist"), "-I", "/opt/local", "-p", staging_path, "-U", "#{cwd}/#{name}-#{self.version}-#{iteration}.tgz" ]
|
26
|
+
safesystem("pkg_create", *args)
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def iteration
|
31
|
+
return @iteration ? @iteration : 1
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require "erb"
|
2
|
+
require "fpm/namespace"
|
3
|
+
require "fpm/package"
|
4
|
+
require "fpm/errors"
|
5
|
+
require "etc"
|
6
|
+
require "fileutils"
|
7
|
+
|
8
|
+
class FPM::Package::Puppet < FPM::Package
|
9
|
+
def architecture
|
10
|
+
case @architecture
|
11
|
+
when nil, "native"
|
12
|
+
@architecture = %x{uname -m}.chomp
|
13
|
+
end
|
14
|
+
return @architecture
|
15
|
+
end # def architecture
|
16
|
+
|
17
|
+
# Default specfile generator just makes one specfile, whatever that is for
|
18
|
+
# this package.
|
19
|
+
def generate_specfile(builddir)
|
20
|
+
paths = []
|
21
|
+
logger.info("PWD: #{File.join(builddir, unpack_data_to)}")
|
22
|
+
fileroot = File.join(builddir, unpack_data_to)
|
23
|
+
Dir.chdir(fileroot) do
|
24
|
+
Find.find(".") do |p|
|
25
|
+
next if p == "."
|
26
|
+
paths << p
|
27
|
+
end
|
28
|
+
end
|
29
|
+
logger.info(paths[-1])
|
30
|
+
manifests = %w{package.pp package/remove.pp}
|
31
|
+
|
32
|
+
::Dir.mkdir(File.join(builddir, "manifests"))
|
33
|
+
manifests.each do |manifest|
|
34
|
+
dir = File.join(builddir, "manifests", File.dirname(manifest))
|
35
|
+
logger.info("manifests targeting: #{dir}")
|
36
|
+
::Dir.mkdir(dir) if !File.directory?(dir)
|
37
|
+
|
38
|
+
File.open(File.join(builddir, "manifests", manifest), "w") do |f|
|
39
|
+
logger.info("manifest: #{f.path}")
|
40
|
+
template = template(File.join("puppet", "#{manifest}.erb"))
|
41
|
+
::Dir.chdir(fileroot) do
|
42
|
+
f.puts template.result(binding)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end # def generate_specfile
|
47
|
+
|
48
|
+
def unpack_data_to
|
49
|
+
"files"
|
50
|
+
end
|
51
|
+
|
52
|
+
def build!(params)
|
53
|
+
# TODO(sissel): Support these somehow, perhaps with execs and files.
|
54
|
+
self.scripts.each do |name, path|
|
55
|
+
case name
|
56
|
+
when "pre-install"
|
57
|
+
when "post-install"
|
58
|
+
when "pre-uninstall"
|
59
|
+
when "post-uninstall"
|
60
|
+
end # case name
|
61
|
+
end # self.scripts.each
|
62
|
+
|
63
|
+
if File.exists?(params[:output])
|
64
|
+
# TODO(sissel): Allow folks to choose output?
|
65
|
+
logger.error("Puppet module directory '#{params[:output]}' already " \
|
66
|
+
"exists. Delete it or choose another output (-p flag)")
|
67
|
+
end
|
68
|
+
|
69
|
+
::Dir.mkdir(params[:output])
|
70
|
+
builddir = ::Dir.pwd
|
71
|
+
|
72
|
+
# Copy 'files' from builddir to :output/files
|
73
|
+
Find.find("files", "manifests") do |path|
|
74
|
+
logger.info("Copying path: #{path}")
|
75
|
+
if File.directory?(path)
|
76
|
+
::Dir.mkdir(File.join(params[:output], path))
|
77
|
+
else
|
78
|
+
FileUtils.cp(path, File.join(params[:output], path))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end # def build!
|
82
|
+
|
83
|
+
# The directory we create should just be the name of the package as the
|
84
|
+
# module name
|
85
|
+
def default_output
|
86
|
+
name
|
87
|
+
end # def default_output
|
88
|
+
|
89
|
+
# This method is used by the puppet manifest template
|
90
|
+
def puppetsort(hash)
|
91
|
+
# TODO(sissel): Implement sorting that follows the puppet style guide
|
92
|
+
# Such as, 'ensure' goes first, etc.
|
93
|
+
return hash.to_a
|
94
|
+
end # def puppetsort
|
95
|
+
|
96
|
+
# Helper for user lookup
|
97
|
+
def uid2user(uid)
|
98
|
+
begin
|
99
|
+
pwent = Etc.getpwuid(uid)
|
100
|
+
return pwent.name
|
101
|
+
rescue ArgumentError => e
|
102
|
+
# Invalid user id? No user? Return the uid.
|
103
|
+
logger.warn("Failed to find username for uid #{uid}")
|
104
|
+
return uid.to_s
|
105
|
+
end
|
106
|
+
end # def uid2user
|
107
|
+
|
108
|
+
# Helper for group lookup
|
109
|
+
def gid2group(gid)
|
110
|
+
begin
|
111
|
+
grent = Etc.getgrgid(gid)
|
112
|
+
return grent.name
|
113
|
+
rescue ArgumentError => e
|
114
|
+
# Invalid user id? No user? Return the uid.
|
115
|
+
logger.warn("Failed to find group for gid #{gid}")
|
116
|
+
return gid.to_s
|
117
|
+
end
|
118
|
+
end # def uid2user
|
119
|
+
end # class FPM::Target::Puppet
|
120
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
__all__ = [ "get_metadata" ]
|
@@ -0,0 +1,104 @@
|
|
1
|
+
from distutils.core import Command
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
import pkg_resources
|
5
|
+
try:
|
6
|
+
import json
|
7
|
+
except ImportError:
|
8
|
+
import simplejson as json
|
9
|
+
|
10
|
+
PY3 = sys.version_info[0] == 3
|
11
|
+
|
12
|
+
if PY3:
|
13
|
+
def u(s):
|
14
|
+
return s
|
15
|
+
else:
|
16
|
+
def u(s):
|
17
|
+
if isinstance(u, unicode):
|
18
|
+
return u
|
19
|
+
return s.decode('utf-8')
|
20
|
+
|
21
|
+
|
22
|
+
# Note, the last time I coded python daily was at Google, so it's entirely
|
23
|
+
# possible some of my techniques below are outdated or bad.
|
24
|
+
# If you have fixes, let me know.
|
25
|
+
|
26
|
+
|
27
|
+
class get_metadata(Command):
|
28
|
+
description = "get package metadata"
|
29
|
+
user_options = [
|
30
|
+
('load-requirements-txt', 'l',
|
31
|
+
"load dependencies from requirements.txt"),
|
32
|
+
("output=", "o", "output destination for metadata json")
|
33
|
+
]
|
34
|
+
boolean_options = ['load-requirements-txt']
|
35
|
+
|
36
|
+
def initialize_options(self):
|
37
|
+
self.load_requirements_txt = False
|
38
|
+
self.cwd = None
|
39
|
+
self.output = None
|
40
|
+
|
41
|
+
def finalize_options(self):
|
42
|
+
self.cwd = os.getcwd()
|
43
|
+
self.requirements_txt = os.path.join(self.cwd, "requirements.txt")
|
44
|
+
# make sure we have a requirements.txt
|
45
|
+
if self.load_requirements_txt:
|
46
|
+
self.load_requirements_txt = os.path.exists(self.requirements_txt)
|
47
|
+
|
48
|
+
def process_dep(self, dep):
|
49
|
+
deps = []
|
50
|
+
if dep.specs:
|
51
|
+
for operator, version in dep.specs:
|
52
|
+
deps.append("%s %s %s" % (dep.project_name,
|
53
|
+
operator, version))
|
54
|
+
else:
|
55
|
+
deps.append(dep.project_name)
|
56
|
+
|
57
|
+
return deps
|
58
|
+
|
59
|
+
def run(self):
|
60
|
+
data = {
|
61
|
+
"name": self.distribution.get_name(),
|
62
|
+
"version": self.distribution.get_version(),
|
63
|
+
"author": u("%s <%s>") % (
|
64
|
+
u(self.distribution.get_author()),
|
65
|
+
u(self.distribution.get_author_email()),
|
66
|
+
),
|
67
|
+
"description": self.distribution.get_description(),
|
68
|
+
"license": self.distribution.get_license(),
|
69
|
+
"url": self.distribution.get_url(),
|
70
|
+
}
|
71
|
+
|
72
|
+
if self.distribution.has_ext_modules():
|
73
|
+
data["architecture"] = "native"
|
74
|
+
else:
|
75
|
+
data["architecture"] = "all"
|
76
|
+
|
77
|
+
final_deps = []
|
78
|
+
|
79
|
+
if self.load_requirements_txt:
|
80
|
+
requirement = open(self.requirements_txt).readlines()
|
81
|
+
for dep in pkg_resources.parse_requirements(requirement):
|
82
|
+
final_deps.extend(self.process_dep(dep))
|
83
|
+
else:
|
84
|
+
if getattr(self.distribution, 'install_requires', None):
|
85
|
+
for dep in pkg_resources.parse_requirements(
|
86
|
+
self.distribution.install_requires):
|
87
|
+
final_deps.extend(self.process_dep(dep))
|
88
|
+
|
89
|
+
data["dependencies"] = final_deps
|
90
|
+
|
91
|
+
output = open(self.output, "w")
|
92
|
+
if hasattr(json, 'dumps'):
|
93
|
+
def default_to_str(obj):
|
94
|
+
""" Fall back to using __str__ if possible """
|
95
|
+
# This checks if the class of obj defines __str__ itself,
|
96
|
+
# so we don't fall back to an inherited __str__ method.
|
97
|
+
if "__str__" in type(obj).__dict__:
|
98
|
+
return str(obj)
|
99
|
+
return json.JSONEncoder.default(self, obj)
|
100
|
+
|
101
|
+
output.write(json.dumps(data, indent=2, default=default_to_str))
|
102
|
+
else:
|
103
|
+
# For Python 2.5 and Debian's python-json
|
104
|
+
output.write(json.write(data))
|
@@ -0,0 +1,317 @@
|
|
1
|
+
require "fpm/namespace"
|
2
|
+
require "fpm/package"
|
3
|
+
require "fpm/util"
|
4
|
+
require "rubygems/package"
|
5
|
+
require "rubygems"
|
6
|
+
require "fileutils"
|
7
|
+
require "tmpdir"
|
8
|
+
require "json"
|
9
|
+
|
10
|
+
# Support for python packages.
|
11
|
+
#
|
12
|
+
# This supports input, but not output.
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
#
|
16
|
+
# # Download the django python package:
|
17
|
+
# pkg = FPM::Package::Python.new
|
18
|
+
# pkg.input("Django")
|
19
|
+
#
|
20
|
+
class FPM::Package::Python < FPM::Package
|
21
|
+
# Flags '--foo' will be accessable as attributes[:python_foo]
|
22
|
+
option "--bin", "PYTHON_EXECUTABLE",
|
23
|
+
"The path to the python executable you wish to run.", :default => "python"
|
24
|
+
option "--easyinstall", "EASYINSTALL_EXECUTABLE",
|
25
|
+
"The path to the easy_install executable tool", :default => "easy_install"
|
26
|
+
option "--pip", "PIP_EXECUTABLE",
|
27
|
+
"The path to the pip executable tool. If not specified, easy_install " \
|
28
|
+
"is used instead", :default => nil
|
29
|
+
option "--pypi", "PYPI_URL",
|
30
|
+
"PyPi Server uri for retrieving packages.",
|
31
|
+
:default => "https://pypi.python.org/simple"
|
32
|
+
option "--package-prefix", "NAMEPREFIX",
|
33
|
+
"(DEPRECATED, use --package-name-prefix) Name to prefix the package " \
|
34
|
+
"name with." do |value|
|
35
|
+
logger.warn("Using deprecated flag: --package-prefix. Please use " \
|
36
|
+
"--package-name-prefix")
|
37
|
+
value
|
38
|
+
end
|
39
|
+
option "--package-name-prefix", "PREFIX", "Name to prefix the package " \
|
40
|
+
"name with.", :default => "python"
|
41
|
+
option "--fix-name", :flag, "Should the target package name be prefixed?",
|
42
|
+
:default => true
|
43
|
+
option "--fix-dependencies", :flag, "Should the package dependencies be " \
|
44
|
+
"prefixed?", :default => true
|
45
|
+
|
46
|
+
option "--downcase-name", :flag, "Should the target package name be in " \
|
47
|
+
"lowercase?", :default => true
|
48
|
+
option "--downcase-dependencies", :flag, "Should the package dependencies " \
|
49
|
+
"be in lowercase?", :default => true
|
50
|
+
|
51
|
+
option "--install-bin", "BIN_PATH", "The path to where python scripts " \
|
52
|
+
"should be installed to."
|
53
|
+
option "--install-lib", "LIB_PATH", "The path to where python libs " \
|
54
|
+
"should be installed to (default depends on your python installation). " \
|
55
|
+
"Want to find out what your target platform is using? Run this: " \
|
56
|
+
"python -c 'from distutils.sysconfig import get_python_lib; " \
|
57
|
+
"print get_python_lib()'"
|
58
|
+
option "--install-data", "DATA_PATH", "The path to where data should be " \
|
59
|
+
"installed to. This is equivalent to 'python setup.py --install-data " \
|
60
|
+
"DATA_PATH"
|
61
|
+
option "--dependencies", :flag, "Include requirements defined in setup.py" \
|
62
|
+
" as dependencies.", :default => true
|
63
|
+
option "--obey-requirements-txt", :flag, "Use a requirements.txt file " \
|
64
|
+
"in the top-level directory of the python package for dependency " \
|
65
|
+
"detection.", :default => false
|
66
|
+
option "--scripts-executable", "PYTHON_EXECUTABLE", "Set custom python " \
|
67
|
+
"interpreter in installing scripts. By default distutils will replace " \
|
68
|
+
"python interpreter in installing scripts (specified by shebang) with " \
|
69
|
+
"current python interpreter (sys.executable). This option is equivalent " \
|
70
|
+
"to appending 'build_scripts --executable PYTHON_EXECUTABLE' arguments " \
|
71
|
+
"to 'setup.py install' command."
|
72
|
+
option "--disable-dependency", "python_package_name",
|
73
|
+
"The python package name to remove from dependency list",
|
74
|
+
:multivalued => true, :attribute_name => :python_disable_dependency,
|
75
|
+
:default => []
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Input a package.
|
80
|
+
#
|
81
|
+
# The 'package' can be any of:
|
82
|
+
#
|
83
|
+
# * A name of a package on pypi (ie; easy_install some-package)
|
84
|
+
# * The path to a directory containing setup.py
|
85
|
+
# * The path to a setup.py
|
86
|
+
def input(package)
|
87
|
+
path_to_package = download_if_necessary(package, version)
|
88
|
+
|
89
|
+
if File.directory?(path_to_package)
|
90
|
+
setup_py = File.join(path_to_package, "setup.py")
|
91
|
+
else
|
92
|
+
setup_py = path_to_package
|
93
|
+
end
|
94
|
+
|
95
|
+
if !File.exist?(setup_py)
|
96
|
+
logger.error("Could not find 'setup.py'", :path => setup_py)
|
97
|
+
raise "Unable to find python package; tried #{setup_py}"
|
98
|
+
end
|
99
|
+
|
100
|
+
load_package_info(setup_py)
|
101
|
+
install_to_staging(setup_py)
|
102
|
+
end # def input
|
103
|
+
|
104
|
+
# Download the given package if necessary. If version is given, that version
|
105
|
+
# will be downloaded, otherwise the latest is fetched.
|
106
|
+
def download_if_necessary(package, version=nil)
|
107
|
+
# TODO(sissel): this should just be a 'download' method, the 'if_necessary'
|
108
|
+
# part should go elsewhere.
|
109
|
+
path = package
|
110
|
+
# If it's a path, assume local build.
|
111
|
+
if File.directory?(path) or (File.exist?(path) and File.basename(path) == "setup.py")
|
112
|
+
return path
|
113
|
+
end
|
114
|
+
|
115
|
+
logger.info("Trying to download", :package => package)
|
116
|
+
|
117
|
+
if version.nil?
|
118
|
+
want_pkg = "#{package}"
|
119
|
+
else
|
120
|
+
want_pkg = "#{package}==#{version}"
|
121
|
+
end
|
122
|
+
|
123
|
+
target = build_path(package)
|
124
|
+
FileUtils.mkdir(target) unless File.directory?(target)
|
125
|
+
|
126
|
+
if attributes[:python_pip].nil?
|
127
|
+
# no pip, use easy_install
|
128
|
+
logger.debug("no pip, defaulting to easy_install", :easy_install => attributes[:python_easyinstall])
|
129
|
+
safesystem(attributes[:python_easyinstall], "-i",
|
130
|
+
attributes[:python_pypi], "--editable", "-U",
|
131
|
+
"--build-directory", target, want_pkg)
|
132
|
+
else
|
133
|
+
logger.debug("using pip", :pip => attributes[:python_pip])
|
134
|
+
safesystem(attributes[:python_pip], "install", "--no-deps", "--no-install", "--no-use-wheel", "-i", attributes[:python_pypi], "-U", "--build", target, want_pkg)
|
135
|
+
end
|
136
|
+
|
137
|
+
# easy_install will put stuff in @tmpdir/packagename/, so find that:
|
138
|
+
# @tmpdir/somepackage/setup.py
|
139
|
+
dirs = ::Dir.glob(File.join(target, "*"))
|
140
|
+
if dirs.length != 1
|
141
|
+
raise "Unexpected directory layout after easy_install. Maybe file a bug? The directory is #{build_path}"
|
142
|
+
end
|
143
|
+
return dirs.first
|
144
|
+
end # def download
|
145
|
+
|
146
|
+
# Load the package information like name, version, dependencies.
|
147
|
+
def load_package_info(setup_py)
|
148
|
+
if !attributes[:python_package_prefix].nil?
|
149
|
+
attributes[:python_package_name_prefix] = attributes[:python_package_prefix]
|
150
|
+
end
|
151
|
+
|
152
|
+
begin
|
153
|
+
json_test_code = [
|
154
|
+
"try:",
|
155
|
+
" import json",
|
156
|
+
"except ImportError:",
|
157
|
+
" import simplejson as json"
|
158
|
+
].join("\n")
|
159
|
+
safesystem("#{attributes[:python_bin]} -c '#{json_test_code}'")
|
160
|
+
rescue FPM::Util::ProcessFailed => e
|
161
|
+
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)
|
162
|
+
raise FPM::Util::ProcessFailed, "Python (#{attributes[:python_bin]}) is missing simplejson or json modules."
|
163
|
+
end
|
164
|
+
|
165
|
+
begin
|
166
|
+
safesystem("#{attributes[:python_bin]} -c 'import pkg_resources'")
|
167
|
+
rescue FPM::Util::ProcessFailed => e
|
168
|
+
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)
|
169
|
+
raise FPM::Util::ProcessFailed, "Python (#{attributes[:python_bin]}) is missing pkg_resources module."
|
170
|
+
end
|
171
|
+
|
172
|
+
# Add ./pyfpm/ to the python library path
|
173
|
+
pylib = File.expand_path(File.dirname(__FILE__))
|
174
|
+
|
175
|
+
# chdir to the directory holding setup.py because some python setup.py's assume that you are
|
176
|
+
# in the same directory.
|
177
|
+
setup_dir = File.dirname(setup_py)
|
178
|
+
|
179
|
+
output = ::Dir.chdir(setup_dir) do
|
180
|
+
tmp = build_path("metadata.json")
|
181
|
+
setup_cmd = "env PYTHONPATH=#{pylib} #{attributes[:python_bin]} " \
|
182
|
+
"setup.py --command-packages=pyfpm get_metadata --output=#{tmp}"
|
183
|
+
|
184
|
+
if attributes[:python_obey_requirements_txt?]
|
185
|
+
setup_cmd += " --load-requirements-txt"
|
186
|
+
end
|
187
|
+
|
188
|
+
# Capture the output, which will be JSON metadata describing this python
|
189
|
+
# package. See fpm/lib/fpm/package/pyfpm/get_metadata.py for more
|
190
|
+
# details.
|
191
|
+
logger.info("fetching package metadata", :setup_cmd => setup_cmd)
|
192
|
+
|
193
|
+
success = safesystem(setup_cmd)
|
194
|
+
#%x{#{setup_cmd}}
|
195
|
+
if !success
|
196
|
+
logger.error("setup.py get_metadata failed", :command => setup_cmd,
|
197
|
+
:exitcode => $?.exitstatus)
|
198
|
+
raise "An unexpected error occurred while processing the setup.py file"
|
199
|
+
end
|
200
|
+
File.read(tmp)
|
201
|
+
end
|
202
|
+
logger.debug("result from `setup.py get_metadata`", :data => output)
|
203
|
+
metadata = JSON.parse(output)
|
204
|
+
logger.info("object output of get_metadata", :json => metadata)
|
205
|
+
|
206
|
+
self.architecture = metadata["architecture"]
|
207
|
+
self.description = metadata["description"]
|
208
|
+
# Sometimes the license field is multiple lines; do best-effort and just
|
209
|
+
# use the first line.
|
210
|
+
self.license = metadata["license"].split(/[\r\n]+/).first
|
211
|
+
self.version = metadata["version"]
|
212
|
+
self.url = metadata["url"]
|
213
|
+
|
214
|
+
# name prefixing is optional, if enabled, a name 'foo' will become
|
215
|
+
# 'python-foo' (depending on what the python_package_name_prefix is)
|
216
|
+
if attributes[:python_fix_name?]
|
217
|
+
self.name = fix_name(metadata["name"])
|
218
|
+
else
|
219
|
+
self.name = metadata["name"]
|
220
|
+
end
|
221
|
+
|
222
|
+
# convert python-Foo to python-foo if flag is set
|
223
|
+
self.name = self.name.downcase if attributes[:python_downcase_name?]
|
224
|
+
|
225
|
+
if !attributes[:no_auto_depends?] and attributes[:python_dependencies?]
|
226
|
+
metadata["dependencies"].each do |dep|
|
227
|
+
dep_re = /^([^<>!= ]+)\s*(?:([<>!=]{1,2})\s*(.*))?$/
|
228
|
+
match = dep_re.match(dep)
|
229
|
+
if match.nil?
|
230
|
+
logger.error("Unable to parse dependency", :dependency => dep)
|
231
|
+
raise FPM::InvalidPackageConfiguration, "Invalid dependency '#{dep}'"
|
232
|
+
end
|
233
|
+
name, cmp, version = match.captures
|
234
|
+
|
235
|
+
next if attributes[:python_disable_dependency].include?(name)
|
236
|
+
|
237
|
+
# convert == to =
|
238
|
+
if cmp == "=="
|
239
|
+
logger.info("Converting == dependency requirement to =", :dependency => dep )
|
240
|
+
cmp = "="
|
241
|
+
end
|
242
|
+
|
243
|
+
# dependency name prefixing is optional, if enabled, a name 'foo' will
|
244
|
+
# become 'python-foo' (depending on what the python_package_name_prefix
|
245
|
+
# is)
|
246
|
+
name = fix_name(name) if attributes[:python_fix_dependencies?]
|
247
|
+
|
248
|
+
# convert dependencies from python-Foo to python-foo
|
249
|
+
name = name.downcase if attributes[:python_downcase_dependencies?]
|
250
|
+
|
251
|
+
self.dependencies << "#{name} #{cmp} #{version}"
|
252
|
+
end
|
253
|
+
end # if attributes[:python_dependencies?]
|
254
|
+
end # def load_package_info
|
255
|
+
|
256
|
+
# Sanitize package name.
|
257
|
+
# Some PyPI packages can be named 'python-foo', so we don't want to end up
|
258
|
+
# with a package named 'python-python-foo'.
|
259
|
+
# But we want packages named like 'pythonweb' to be suffixed
|
260
|
+
# 'python-pythonweb'.
|
261
|
+
def fix_name(name)
|
262
|
+
if name.start_with?("python")
|
263
|
+
# If the python package is called "python-foo" strip the "python-" part while
|
264
|
+
# prepending the package name prefix.
|
265
|
+
return [attributes[:python_package_name_prefix], name.gsub(/^python-/, "")].join("-")
|
266
|
+
else
|
267
|
+
return [attributes[:python_package_name_prefix], name].join("-")
|
268
|
+
end
|
269
|
+
end # def fix_name
|
270
|
+
|
271
|
+
# Install this package to the staging directory
|
272
|
+
def install_to_staging(setup_py)
|
273
|
+
project_dir = File.dirname(setup_py)
|
274
|
+
|
275
|
+
prefix = "/"
|
276
|
+
prefix = attributes[:prefix] unless attributes[:prefix].nil?
|
277
|
+
|
278
|
+
# Some setup.py's assume $PWD == current directory of setup.py, so let's
|
279
|
+
# chdir first.
|
280
|
+
::Dir.chdir(project_dir) do
|
281
|
+
flags = [ "--root", staging_path ]
|
282
|
+
if !attributes[:python_install_lib].nil?
|
283
|
+
flags += [ "--install-lib", File.join(prefix, attributes[:python_install_lib]) ]
|
284
|
+
elsif !attributes[:prefix].nil?
|
285
|
+
# setup.py install --prefix PREFIX still installs libs to
|
286
|
+
# PREFIX/lib64/python2.7/site-packages/
|
287
|
+
# but we really want something saner.
|
288
|
+
#
|
289
|
+
# since prefix is given, but not python_install_lib, assume PREFIX/lib
|
290
|
+
flags += [ "--install-lib", File.join(prefix, "lib") ]
|
291
|
+
end
|
292
|
+
|
293
|
+
if !attributes[:python_install_data].nil?
|
294
|
+
flags += [ "--install-data", File.join(prefix, attributes[:python_install_data]) ]
|
295
|
+
elsif !attributes[:prefix].nil?
|
296
|
+
# prefix given, but not python_install_data, assume PREFIX/data
|
297
|
+
flags += [ "--install-data", File.join(prefix, "data") ]
|
298
|
+
end
|
299
|
+
|
300
|
+
if !attributes[:python_install_bin].nil?
|
301
|
+
flags += [ "--install-scripts", File.join(prefix, attributes[:python_install_bin]) ]
|
302
|
+
elsif !attributes[:prefix].nil?
|
303
|
+
# prefix given, but not python_install_bin, assume PREFIX/bin
|
304
|
+
flags += [ "--install-scripts", File.join(prefix, "bin") ]
|
305
|
+
end
|
306
|
+
|
307
|
+
if !attributes[:python_scripts_executable].nil?
|
308
|
+
# Overwrite installed python scripts shebang binary with provided executable
|
309
|
+
flags += [ "build_scripts", "--executable", attributes[:python_scripts_executable] ]
|
310
|
+
end
|
311
|
+
|
312
|
+
safesystem(attributes[:python_bin], "setup.py", "install", *flags)
|
313
|
+
end
|
314
|
+
end # def install_to_staging
|
315
|
+
|
316
|
+
public(:input)
|
317
|
+
end # class FPM::Package::Python
|