-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
python.rb
240 lines (214 loc) · 8.97 KB
/
python.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
require "fpm/namespace"
require "fpm/package"
require "fpm/util"
require "rubygems/package"
require "rubygems"
require "fileutils"
require "tmpdir"
require "json"
# Support for python packages.
#
# This supports input, but not output.
#
# Example:
#
# # Download the django python package:
# pkg = FPM::Package::Python.new
# pkg.input("Django")
#
class FPM::Package::Python < FPM::Package
# Flags '--foo' will be accessable as attributes[:python_foo]
option "--bin", "PYTHON_EXECUTABLE",
"The path to the python executable you wish to run.", :default => "python"
option "--easyinstall", "EASYINSTALL_EXECUTABLE",
"The path to the easy_install executable tool", :default => "easy_install"
option "--pip", "PIP_EXECUTABLE",
"The path to the pip executable tool. If not specified, easy_install " \
"is used instead", :default => nil
option "--pypi", "PYPI_URL",
"PyPi Server uri for retrieving packages.",
:default => "http://pypi.python.org/simple"
option "--package-prefix", "NAMEPREFIX",
"(DEPRECATED, use --package-name-prefix) Name to prefix the package " \
"name with." do |value|
@logger.warn("Using deprecated flag: --package-prefix. Please use " \
"--package-name-prefix")
value
end
option "--package-name-prefix", "PREFIX", "Name to prefix the package " \
"name with.", :default => "python"
option "--fix-name", :flag, "Should the target package name be prefixed?",
:default => true
option "--fix-dependencies", :flag, "Should the package dependencies be " \
"prefixed?", :default => true
option "--install-bin", "BIN_PATH", "The path to where python scripts " \
"should be installed to.", :default => "/usr/bin"
option "--install-lib", "LIB_PATH", "The path to where python libs " \
"should be installed to (default depends on your python installation). " \
"Want to what your target platform is using? Run this: " \
"python -c 'from distutils.sysconfig import get_python_lib; " \
"print get_python_lib()'"
option "--install-data", "DATA_PATH", "The path to where data should be." \
"installed to. This is equivalent to 'python setup.py --install-data " \
"DATA_PATH"
private
# Input a package.
#
# The 'package' can be any of:
#
# * A name of a package on pypi (ie; easy_install some-package)
# * The path to a directory containing setup.py
# * The path to a setup.py
def input(package)
path_to_package = download_if_necessary(package, version)
if File.directory?(path_to_package)
setup_py = File.join(path_to_package, "setup.py")
else
setup_py = path_to_package
end
if !File.exists?(setup_py)
@logger.error("Could not find 'setup.py'", :path => setup_py)
raise "Unable to find python package; tried #{setup_py}"
end
load_package_info(setup_py)
install_to_staging(setup_py)
end # def input
# Download the given package if necessary. If version is given, that version
# will be downloaded, otherwise the latest is fetched.
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
# If it's a path, assume local build.
if File.directory?(path) or (File.exists?(path) and File.basename(path) == "setup.py")
return path
end
@logger.info("Trying to download", :package => package)
if version.nil?
want_pkg = "#{package}"
else
want_pkg = "#{package}==#{version}"
end
target = build_path(package)
FileUtils.mkdir(target) unless File.directory?(target)
if attributes[:python_pip].nil?
# no pip, use easy_install
puts "EASY_INSTALL"
safesystem(attributes[:python_easyinstall], "-i",
attributes[:python_pypi], "--editable", "-U",
"--build-directory", target, want_pkg)
else
puts "PIP PIP CHEERIOS"
safesystem(attributes[:python_pip], "install", "--no-install",
"-U", "--build", target, want_pkg)
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
# Load the package information like name, version, dependencies.
def load_package_info(setup_py)
if !attributes[:python_package_prefix].nil?
attributes[:python_package_name_prefix] = attributes[:python_package_prefix]
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
setup_cmd = "env PYTHONPATH=#{pylib} #{attributes[:python_bin]} " \
"setup.py --command-packages=pyfpm get_metadata"
# Capture the output, which will be JSON metadata describing this python
# package. See fpm/lib/fpm/package/pyfpm/get_metadata.py for more
# details.
output = `#{setup_cmd}`
if !$?.success?
@logger.error("setup.py get_metadata failed", :command => setup_cmd,
:exitcode => $?.exitcode)
raise "An unexpected error occurred while processing the setup.py file"
end
output
end
@logger.debug("full text from `setup.py get_metadata`", :data => output)
metadata = JSON.parse(output[/\{.*\}/msx])
@logger.info("object output of get_metadata", :json => metadata)
self.architecture = metadata["architecture"]
self.description = metadata["description"]
self.license = metadata["license"]
self.version = metadata["version"]
self.url = metadata["url"]
# name prefixing is optional, if enabled, a name 'foo' will become
# 'python-foo' (depending on what the python_package_name_prefix is)
if attributes[:python_fix_name?]
self.name = fix_name(metadata["name"])
else
self.name = metadata["name"]
end
requirements_txt = File.join(setup_dir, "requirements.txt")
if File.exists?(requirements_txt)
@logger.info("Found requirements.txt, using it instead of setup.py " \
"for dependency information", :path => requirements_txt)
@logger.debug("Clearing dependency list (from setup.py) in prep for " \
"reading requirements.txt")
# Best I can tell, requirements.txt are a superset of what
# is already supported as 'dependencies' in setup.py
# So we'll parse them the same way below.
metadata["dependencies"] = File.read(requirements_txt).split("\n")
end
self.dependencies += metadata["dependencies"].collect do |dep|
dep_re = /^([^<>= ]+)\s*(?:([<>=]{1,2})\s*(.*))?$/
match = dep_re.match(dep)
if match.nil?
@logger.error("Unable to parse dependency", :dependency => dep)
raise FPM::InvalidPackageConfiguration, "Invalid dependency '#{dep}'"
end
name, cmp, version = match.captures
# dependency name prefixing is optional, if enabled, a name 'foo' will
# become 'python-foo' (depending on what the python_package_name_prefix
# is)
name = fix_name(name) if attributes[:python_fix_dependencies?]
"#{name} #{cmp} #{version}"
end
end # def load_package_info
# Sanitize package name.
# Some PyPI packages can be named 'python-foo', so we don't want to end up
# with a package named 'python-python-foo'.
# But we want packages named like 'pythonweb' to be suffixed
# 'python-pythonweb'.
def fix_name(name)
if name.start_with?("python")
# If the python package is called "python-foo" strip the "python-" part while
# prepending the package name prefix.
return [attributes[:python_package_name_prefix], name.gsub(/^python-/, "")].join("-")
else
return [attributes[:python_package_name_prefix], name].join("-")
end
end # def fix_name
# Install this package to the staging directory
def install_to_staging(setup_py)
project_dir = File.dirname(setup_py)
prefix = "/"
prefix = attributes[:prefix] unless attributes[:prefix].nil?
# Some setup.py's assume $PWD == current directory of setup.py, so let's
# chdir first.
::Dir.chdir(project_dir) do
flags = [ "--root", staging_path ]
if !attributes[:python_install_lib].nil?
flags += [ "--install-lib", File.join(prefix, attributes[:python_install_lib]) ]
end
if !attributes[:python_install_data].nil?
flags += [ "--install-data", File.join(prefix, attributes[:python_install_data]) ]
end
if !attributes[:python_install_bin].nil?
flags += [ "--install-scripts", File.join(prefix, attributes[:python_install_bin]) ]
end
safesystem(attributes[:python_bin], "setup.py", "install", *flags)
end
end # def install_to_staging
public(:input)
end # class FPM::Package::Python