-
Notifications
You must be signed in to change notification settings - Fork 4
/
__init__.py
226 lines (195 loc) · 8.57 KB
/
__init__.py
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
##############################################################################
#
# Copyright (c) 2006-2011 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
from hashlib import sha1
import logging
import os
import os.path
import re
import subprocess
import setuptools.archive_util
import shutil
import tempfile
import zc.buildout
import zc.buildout.download
almost_environment_setting = re.compile(r'\w+=').match
not_starting_with_digit = re.compile(r'\D').match
def system(c):
subprocess.check_call(c, shell=True)
class Recipe(object):
def __init__(self, buildout, name, options):
self.buildout, self.name, self.options = buildout, name, options
directory = buildout['buildout']['directory']
download_cache = buildout['buildout'].get('download-cache')
self.url = self.options['url']
extra_options = self.options.get('extra_options', '')
# get rid of any newlines that may be in the options so they
# do not get passed through to the commandline
self.extra_options = ' '.join(extra_options.split())
self.autogen = self.options.get('autogen', '')
self.patch = self.options.get('patch', '')
self.patch_options = self.options.get('patch_options', '-p0')
environ = []
for token in self.options.get('environment', '').split():
if (almost_environment_setting(token) and
not_starting_with_digit(token)):
environ.append(token)
else:
if environ:
environ[-1] += ' ' + token
else:
raise ValueError('Bad environment setting', token)
if environ:
self.environ = dict([x.split('=', 1) for x in environ])
else:
self.environ = {}
self.source_directory_contains = self.options.get(
'source-directory-contains', 'configure')
self.configure_cmd = self.options.get(
'configure-command', './configure')
self.configure_options = self.options.get('configure-options', None)
if self.configure_options:
self.configure_options = ' '.join(self.configure_options.split())
self.shared = options.get('shared', None)
if self.shared:
if os.path.isdir(self.shared):
# to prevent nasty surprises, don't use the directory directly
# since we remove it in case of build errors
self.shared = os.path.join(self.shared, 'cmmi')
else:
if not download_cache:
raise ValueError(
"Set the 'shared' option of zc.recipe.cmmi"
" to an existing"
" directory, or set ${buildout:download-cache}")
self.shared = os.path.join(
directory, download_cache, 'cmmi', 'build')
self.shared = os.path.join(self.shared, self._state_hash())
location = self.shared
else:
location = os.path.join(options.get(
'location', buildout['buildout']['parts-directory']), name)
options['location'] = location
def _state_hash(self):
# hash of our configuration state, so that e.g. different
# ./configure options will get a different build directory.
# Be sure to sort to keep a consistent order, since dictionary
# iteration order is never guaranteed.
env = ''.join(['%s%s' % (key, value) for key, value
in sorted(self.environ.items())])
state = [self.url, self.extra_options, self.autogen,
self.patch, self.patch_options, env]
data = ''.join(state)
if not isinstance(data, bytes):
data = data.encode('utf-8')
return sha1(data).hexdigest()
def install(self):
self.build()
if self.shared:
return ''
return self.options['location']
def update(self):
if not os.path.isdir(self.options['location']):
self.build()
def build(self):
logger = logging.getLogger(self.name)
download = zc.buildout.download.Download(
self.buildout['buildout'], namespace='cmmi', hash_name=True,
logger=logger)
if self.shared:
if os.path.isdir(self.shared):
logger.info('using existing shared build')
return self.shared
fname, is_temp = download(self.url, md5sum=self.options.get('md5sum'))
# now unpack and work as normal
tmp = tempfile.mkdtemp('buildout-' + self.name)
logger.info('Unpacking and configuring')
try:
setuptools.archive_util.unpack_archive(fname, tmp)
finally:
if is_temp:
os.remove(fname)
for key, value in sorted(self.environ.items()):
logger.info('Updating environment: %s=%s', key, value)
os.environ.update(self.environ)
# XXX This is probably more complicated than it needs to be. I
# retained the distinction between makedirs and mkdir when I moved
# creation of the build dir after downloading the source since I
# didn't understand the reason for the distinction. (tlotze)
if self.shared and not os.path.isdir(self.shared):
os.makedirs(self.shared)
dest = self.options['location']
if not os.path.exists(dest):
os.mkdir(dest)
try:
here = os.getcwd()
os.chdir(tmp)
try:
if not (os.path.exists(self.source_directory_contains) or
(self.autogen and os.path.exists(self.autogen))):
entries = os.listdir(tmp)
if len(entries) == 1 and os.path.isdir(entries[0]):
os.chdir(entries[0])
if self.patch != '':
# patch may be a filesystem path or url
# url patches can go through the cache
if self.patch != '':
try:
self.patch, is_temp = download(
self.patch,
md5sum=self.options.get('patch-md5sum'))
except BaseException:
# If download/checksum of the patch fails, leaving
# the tmp dir won't be helpful.
shutil.rmtree(tmp)
raise
try:
system("patch %s < %s"
% (self.patch_options, self.patch))
finally:
if is_temp:
os.remove(self.patch)
if self.autogen != '':
logger.info('auto generating configure files')
system("./%s" % self.autogen)
if not os.path.exists(self.source_directory_contains):
entries = os.listdir(tmp)
if len(entries) == 1 and os.path.isdir(entries[0]):
os.chdir(entries[0])
else:
raise ValueError("Couldn't find configure")
self.cmmi(dest)
shutil.rmtree(tmp)
finally:
os.chdir(here)
except BaseException:
shutil.rmtree(dest)
if os.path.exists(tmp):
logger.error("cmmi failed: %s", tmp)
raise
def cmmi(self, dest):
"""Do the 'configure; make; make install' command sequence.
When this is called, the current working directory is the
source directory. The 'dest' parameter specifies the
installation prefix.
This can be overridden by subclasses to support packages whose
command sequence is different.
"""
options = self.configure_options
if options is None:
options = '--prefix="%s"' % dest
if self.extra_options:
options += ' %s' % self.extra_options
system("%s %s" % (self.configure_cmd, options))
system("make")
system("make install")