blob: 21bbcf8567ad6b3994de678956e2b757922c197a [file] [log] [blame]
Tarek Ziade1231a4e2011-05-19 13:07:25 +02001"""Utilities to find and read config files used by packaging."""
2
3import os
4import sys
5import logging
6
7from shlex import split
8from configparser import RawConfigParser
9from packaging import logger
10from packaging.errors import PackagingOptionError
11from packaging.compiler.extension import Extension
Éric Araujo1c1d9a52011-06-10 23:26:31 +020012from packaging.util import (check_environ, iglob, resolve_name, strtobool,
13 split_multiline)
Tarek Ziade1231a4e2011-05-19 13:07:25 +020014from packaging.compiler import set_compiler
15from packaging.command import set_command
16from packaging.markers import interpret
17
18
19def _pop_values(values_dct, key):
20 """Remove values from the dictionary and convert them as a list"""
21 vals_str = values_dct.pop(key, '')
22 if not vals_str:
23 return
24 fields = []
Tarek Ziade91f0e342011-05-21 12:00:10 +020025 # the line separator is \n for setup.cfg files
26 for field in vals_str.split('\n'):
Tarek Ziade1231a4e2011-05-19 13:07:25 +020027 tmp_vals = field.split('--')
28 if len(tmp_vals) == 2 and not interpret(tmp_vals[1]):
29 continue
30 fields.append(tmp_vals[0])
31 # Get bash options like `gcc -print-file-name=libgcc.a` XXX bash options?
32 vals = split(' '.join(fields))
33 if vals:
34 return vals
35
36
37def _rel_path(base, path):
Tarek Ziadeec9b76d2011-05-21 11:48:16 +020038 # normalizes and returns a lstripped-/-separated path
39 base = base.replace(os.path.sep, '/')
40 path = path.replace(os.path.sep, '/')
Tarek Ziade1231a4e2011-05-19 13:07:25 +020041 assert path.startswith(base)
42 return path[len(base):].lstrip('/')
43
44
45def get_resources_dests(resources_root, rules):
46 """Find destinations for resources files"""
47 destinations = {}
48 for base, suffix, dest in rules:
49 prefix = os.path.join(resources_root, base)
50 for abs_base in iglob(prefix):
51 abs_glob = os.path.join(abs_base, suffix)
52 for abs_path in iglob(abs_glob):
53 resource_file = _rel_path(resources_root, abs_path)
54 if dest is None: # remove the entry if it was here
55 destinations.pop(resource_file, None)
56 else:
57 rel_path = _rel_path(abs_base, abs_path)
Tarek Ziadeec9b76d2011-05-21 11:48:16 +020058 rel_dest = dest.replace(os.path.sep, '/').rstrip('/')
59 destinations[resource_file] = rel_dest + '/' + rel_path
Tarek Ziade1231a4e2011-05-19 13:07:25 +020060 return destinations
61
62
63class Config:
Éric Araujo643cb732011-06-11 00:33:38 +020064 """Class used to work with configuration files"""
Tarek Ziade1231a4e2011-05-19 13:07:25 +020065 def __init__(self, dist):
66 self.dist = dist
Éric Araujo643cb732011-06-11 00:33:38 +020067 self.setup_hooks = []
Tarek Ziade1231a4e2011-05-19 13:07:25 +020068
Éric Araujo643cb732011-06-11 00:33:38 +020069 def run_hooks(self, config):
70 """Run setup hooks in the order defined in the spec."""
71 for hook in self.setup_hooks:
72 hook(config)
Tarek Ziade1231a4e2011-05-19 13:07:25 +020073
74 def find_config_files(self):
75 """Find as many configuration files as should be processed for this
76 platform, and return a list of filenames in the order in which they
77 should be parsed. The filenames returned are guaranteed to exist
78 (modulo nasty race conditions).
79
80 There are three possible config files: packaging.cfg in the
81 Packaging installation directory (ie. where the top-level
82 Packaging __inst__.py file lives), a file in the user's home
83 directory named .pydistutils.cfg on Unix and pydistutils.cfg
84 on Windows/Mac; and setup.cfg in the current directory.
85
86 The file in the user's home directory can be disabled with the
87 --no-user-cfg option.
88 """
89 files = []
90 check_environ()
91
92 # Where to look for the system-wide Packaging config file
93 sys_dir = os.path.dirname(sys.modules['packaging'].__file__)
94
95 # Look for the system config file
96 sys_file = os.path.join(sys_dir, "packaging.cfg")
97 if os.path.isfile(sys_file):
98 files.append(sys_file)
99
100 # What to call the per-user config file
101 if os.name == 'posix':
102 user_filename = ".pydistutils.cfg"
103 else:
104 user_filename = "pydistutils.cfg"
105
106 # And look for the user config file
107 if self.dist.want_user_cfg:
108 user_file = os.path.join(os.path.expanduser('~'), user_filename)
109 if os.path.isfile(user_file):
110 files.append(user_file)
111
112 # All platforms support local setup.cfg
113 local_file = "setup.cfg"
114 if os.path.isfile(local_file):
115 files.append(local_file)
116
117 if logger.isEnabledFor(logging.DEBUG):
118 logger.debug("using config files: %s", ', '.join(files))
119 return files
120
121 def _convert_metadata(self, name, value):
122 # converts a value found in setup.cfg into a valid metadata
123 # XXX
124 return value
125
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200126 def _read_setup_cfg(self, parser, cfg_filename):
127 cfg_directory = os.path.dirname(os.path.abspath(cfg_filename))
128 content = {}
129 for section in parser.sections():
130 content[section] = dict(parser.items(section))
131
Éric Araujo643cb732011-06-11 00:33:38 +0200132 # global setup hooks are called first
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200133 if 'global' in content:
Éric Araujo643cb732011-06-11 00:33:38 +0200134 if 'setup_hooks' in content['global']:
135 setup_hooks = split_multiline(content['global']['setup_hooks'])
136
137 for line in setup_hooks:
138 try:
139 hook = resolve_name(line)
140 except ImportError as e:
141 logger.warning('cannot find setup hook: %s', e.args[0])
142 else:
143 self.setup_hooks.append(hook)
144
145 self.run_hooks(content)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200146
147 metadata = self.dist.metadata
148
149 # setting the metadata values
150 if 'metadata' in content:
151 for key, value in content['metadata'].items():
152 key = key.replace('_', '-')
153 if metadata.is_multi_field(key):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200154 value = split_multiline(value)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200155
156 if key == 'project-url':
157 value = [(label.strip(), url.strip())
158 for label, url in
159 [v.split(',') for v in value]]
160
161 if key == 'description-file':
162 if 'description' in content['metadata']:
163 msg = ("description and description-file' are "
164 "mutually exclusive")
165 raise PackagingOptionError(msg)
166
Éric Araujo8474f292011-06-11 00:21:18 +0200167 filenames = value.split()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200168
Éric Araujo8474f292011-06-11 00:21:18 +0200169 # concatenate all files
170 value = []
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200171 for filename in filenames:
172 # will raise if file not found
173 with open(filename) as description_file:
Éric Araujo8474f292011-06-11 00:21:18 +0200174 value.append(description_file.read().strip())
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200175 # add filename as a required file
176 if filename not in metadata.requires_files:
177 metadata.requires_files.append(filename)
Éric Araujo8474f292011-06-11 00:21:18 +0200178 value = '\n'.join(value).strip()
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200179 key = 'description'
180
181 if metadata.is_metadata_field(key):
182 metadata[key] = self._convert_metadata(key, value)
183
184 if 'files' in content:
185 files = content['files']
186 self.dist.package_dir = files.pop('packages_root', None)
187
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200188 files = dict((key, split_multiline(value)) for key, value in
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200189 files.items())
190
191 self.dist.packages = []
192
193 packages = files.get('packages', [])
194 if isinstance(packages, str):
195 packages = [packages]
196
197 for package in packages:
198 if ':' in package:
199 dir_, package = package.split(':')
200 self.dist.package_dir[package] = dir_
201 self.dist.packages.append(package)
202
203 self.dist.py_modules = files.get('modules', [])
204 if isinstance(self.dist.py_modules, str):
205 self.dist.py_modules = [self.dist.py_modules]
206 self.dist.scripts = files.get('scripts', [])
207 if isinstance(self.dist.scripts, str):
208 self.dist.scripts = [self.dist.scripts]
209
210 self.dist.package_data = {}
211 for data in files.get('package_data', []):
212 data = data.split('=')
213 if len(data) != 2:
214 continue # XXX error should never pass silently
215 key, value = data
216 self.dist.package_data[key.strip()] = value.strip()
217
218 self.dist.data_files = []
219 for data in files.get('data_files', []):
220 data = data.split('=')
221 if len(data) != 2:
222 continue
223 key, value = data
224 values = [v.strip() for v in value.split(',')]
225 self.dist.data_files.append((key, values))
226
227 # manifest template
228 self.dist.extra_files = files.get('extra_files', [])
229
230 resources = []
231 for rule in files.get('resources', []):
232 glob, destination = rule.split('=', 1)
233 rich_glob = glob.strip().split(' ', 1)
234 if len(rich_glob) == 2:
235 prefix, suffix = rich_glob
236 else:
237 assert len(rich_glob) == 1
238 prefix = ''
239 suffix = glob
240 if destination == '<exclude>':
241 destination = None
242 resources.append(
243 (prefix.strip(), suffix.strip(), destination.strip()))
244 self.dist.data_files = get_resources_dests(
245 cfg_directory, resources)
246
247 ext_modules = self.dist.ext_modules
248 for section_key in content:
249 labels = section_key.split('=')
250 if len(labels) == 2 and labels[0] == 'extension':
251 # labels[1] not used from now but should be implemented
252 # for extension build dependency
253 values_dct = content[section_key]
254 ext_modules.append(Extension(
255 values_dct.pop('name'),
256 _pop_values(values_dct, 'sources'),
257 _pop_values(values_dct, 'include_dirs'),
258 _pop_values(values_dct, 'define_macros'),
259 _pop_values(values_dct, 'undef_macros'),
260 _pop_values(values_dct, 'library_dirs'),
261 _pop_values(values_dct, 'libraries'),
262 _pop_values(values_dct, 'runtime_library_dirs'),
263 _pop_values(values_dct, 'extra_objects'),
264 _pop_values(values_dct, 'extra_compile_args'),
265 _pop_values(values_dct, 'extra_link_args'),
266 _pop_values(values_dct, 'export_symbols'),
267 _pop_values(values_dct, 'swig_opts'),
268 _pop_values(values_dct, 'depends'),
269 values_dct.pop('language', None),
270 values_dct.pop('optional', None),
271 **values_dct))
272
273 def parse_config_files(self, filenames=None):
274 if filenames is None:
275 filenames = self.find_config_files()
276
277 logger.debug("Distribution.parse_config_files():")
278
279 parser = RawConfigParser()
280
281 for filename in filenames:
282 logger.debug(" reading %s", filename)
Victor Stinnerdd13dd42011-05-19 18:45:32 +0200283 parser.read(filename, encoding='utf-8')
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200284
285 if os.path.split(filename)[-1] == 'setup.cfg':
286 self._read_setup_cfg(parser, filename)
287
288 for section in parser.sections():
289 if section == 'global':
290 if parser.has_option('global', 'compilers'):
291 self._load_compilers(parser.get('global', 'compilers'))
292
293 if parser.has_option('global', 'commands'):
294 self._load_commands(parser.get('global', 'commands'))
295
296 options = parser.options(section)
297 opt_dict = self.dist.get_option_dict(section)
298
299 for opt in options:
300 if opt == '__name__':
301 continue
302 val = parser.get(section, opt)
303 opt = opt.replace('-', '_')
304
305 if opt == 'sub_commands':
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200306 val = split_multiline(val)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200307 if isinstance(val, str):
308 val = [val]
309
310 # Hooks use a suffix system to prevent being overriden
311 # by a config file processed later (i.e. a hook set in
312 # the user config file cannot be replaced by a hook
313 # set in a project config file, unless they have the
314 # same suffix).
315 if (opt.startswith("pre_hook.") or
316 opt.startswith("post_hook.")):
317 hook_type, alias = opt.split(".")
318 hook_dict = opt_dict.setdefault(
319 hook_type, (filename, {}))[1]
320 hook_dict[alias] = val
321 else:
322 opt_dict[opt] = filename, val
323
324 # Make the RawConfigParser forget everything (so we retain
325 # the original filenames that options come from)
326 parser.__init__()
327
328 # If there was a "global" section in the config file, use it
329 # to set Distribution options.
330 if 'global' in self.dist.command_options:
331 for opt, (src, val) in self.dist.command_options['global'].items():
332 alias = self.dist.negative_opt.get(opt)
333 try:
334 if alias:
335 setattr(self.dist, alias, not strtobool(val))
336 elif opt == 'dry_run': # FIXME ugh!
337 setattr(self.dist, opt, strtobool(val))
338 else:
339 setattr(self.dist, opt, val)
340 except ValueError as msg:
341 raise PackagingOptionError(msg)
342
343 def _load_compilers(self, compilers):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200344 compilers = split_multiline(compilers)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200345 if isinstance(compilers, str):
346 compilers = [compilers]
347 for compiler in compilers:
348 set_compiler(compiler.strip())
349
350 def _load_commands(self, commands):
Éric Araujo1c1d9a52011-06-10 23:26:31 +0200351 commands = split_multiline(commands)
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200352 if isinstance(commands, str):
353 commands = [commands]
354 for command in commands:
355 set_command(command.strip())