summaryrefslogtreecommitdiff
path: root/attoconf/core.py
diff options
context:
space:
mode:
Diffstat (limited to 'attoconf/core.py')
-rw-r--r--attoconf/core.py239
1 files changed, 239 insertions, 0 deletions
diff --git a/attoconf/core.py b/attoconf/core.py
new file mode 100644
index 0000000..efc952b
--- /dev/null
+++ b/attoconf/core.py
@@ -0,0 +1,239 @@
+# Copyright 2013-2014 Ben Longbons <b.r.longbons@gmail.com>
+#
+# This file is part of attoconf.
+#
+# attoconf is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# attoconf is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with attoconf. If not, see <http://www.gnu.org/licenses/>.
+
+
+
+from collections import namedtuple, OrderedDict
+import os
+import sys
+
+from .help import Help
+
+# Nothing to see here. Move along.
+Option = namedtuple('Option', ['type', 'init'])
+class ArgumentError(Exception): pass
+
+def as_var(name):
+ return name.lstrip('-').replace('-', '_').upper()
+
+def trim_trailing_slashes(path):
+ p, s = os.path.split(path)
+ if not s:
+ return p
+ return path
+
+class Project(object):
+ ''' A Project is a directory and the list of options that can be set.
+ '''
+ __slots__ = (
+ 'srcdir',
+ 'aliases',
+ 'options',
+ 'help',
+ 'order',
+ 'checks',
+ )
+ def __init__(self, srcdir):
+ ''' A Project is initially constructed from just the source directory
+ '''
+ self.srcdir = trim_trailing_slashes(srcdir)
+ self.aliases = {}
+ self.options = {}
+ self.help = Help()
+ self.order = []
+ self.checks = []
+
+ def add_help(self, text, hidden):
+ ''' Directly add a line of text to the help.
+ '''
+ self.help.add_text(text, hidden)
+
+ def add_alias(self, key, expansion, help, hidden):
+ ''' Add an alias.
+
+ This is necessary for anything to appear without an =.
+
+ The expansion is a list of other options, which may be aliases.
+ '''
+ if key in self.aliases:
+ raise KeyError(key)
+ expansion = list(expansion)
+ self.aliases[key] = expansion
+ if help is None:
+ help = 'alias for ' + ' '.join(expansion)
+ self.help.add_option(key, help, hidden)
+
+ def add_option(self, name, init, type, check,
+ help, hidden,
+ help_var=None, help_def=None):
+ ''' Add an actual option.
+
+ This must be passed with a =.
+
+ In the builder, the var will first be set to init.
+
+ If the argument is passed, the type hook is called immediately
+ to validate the argument.
+
+ The check hooks will be called at final time,
+ in the order they were added.
+
+ Additionally, a line of help is added, with additional formatting.
+ '''
+ if name in self.options:
+ raise KeyError(name)
+ var = as_var(name)
+ if check is not None:
+ assert type.__module__ == 'attoconf.types'
+ else:
+ # used by some tests ... should this be fixed there instead?
+ if help_var is None:
+ help_var = var
+ assert init is not None
+ init = type(init)
+ assert init is not None
+ self.options[name] = Option(init=init, type=type)
+ if check is not None:
+ self.order.append(var)
+ self.checks.append(
+ lambda bld: check(bld, **{help_var: bld.vars[var]}))
+
+ if help_var is None:
+ help_var = var
+
+ if help_def is None:
+ help_def = init
+ assert help_def is not None
+ help = '%s [%s]' % (help, help_def)
+
+ if help_var != name:
+ help_opt = '%s=%s' % (name, help_var)
+ else:
+ help_opt = name
+ self.help.add_option(help_opt, help, hidden)
+
+ def do_help(self, opt):
+ ''' Pseudo type-hook to be registered for --help (calls sys.exit).
+ '''
+ if opt == 'none':
+ return opt
+ if opt == 'default':
+ hidden = False
+ elif opt == 'hidden':
+ hidden = True
+ else:
+ raise ValueError('Unknown value for opt: %r' % opt)
+ self.help.print(sys.stdout, hidden)
+ sys.exit()
+# sneaky
+Project.do_help.__module__ = 'attoconf.types'
+#Project.do_help.__func__.__module__ = 'attoconf.types'
+
+class Build(object):
+ ''' A Build is a directory and set of options applied to a Project.
+ '''
+ __slots__ = (
+ 'builddir',
+ 'project',
+ 'vars',
+ '_seen_args',
+ )
+ def __init__(self, project, builddir):
+ ''' A Build is initially constructed from a project and a build dir.
+ '''
+ self.project = project
+ self.builddir = trim_trailing_slashes(builddir)
+ self.vars = {as_var(k): o.init
+ for (k, o) in project.options.items()}
+ self._seen_args = OrderedDict()
+
+ def apply_arg(self, arg):
+ ''' Parse a single argument, expanding aliases.
+ '''
+ alias = self.project.aliases.get(arg)
+ if alias is not None:
+ for e in alias:
+ # TODO: catch recursive aliases
+ # or should they be caught earlier?
+ self.apply_arg(e)
+ return
+
+ if '=' not in arg:
+ if arg in self.project.options:
+ raise ArgumentError('Option %s requires an argument' % arg)
+ elif arg.startswith('-'):
+ raise ArgumentError('Unknown option %s' % arg)
+ else:
+ raise ArgumentError('Unknown environment variable %s' % arg)
+
+ k, a = arg.split('=', 1)
+ if k in self._seen_args:
+ del self._seen_args[k]
+ self._seen_args[k] = a
+ opt = self.project.options.get(k)
+ if opt is None:
+ raise sys.exit('Unknown option %s' % k)
+ self.vars[as_var(k)] = opt.type(a)
+
+ def finish(self):
+ ''' With the current set of variables, run all the checks
+ and presumably produce some sort of output.
+ '''
+ for check in self.project.checks:
+ check(self)
+ status_file = os.path.join(self.builddir, 'config.status')
+ # open fd to control +x mode
+ status_fd = os.open(status_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o777)
+ with os.fdopen(status_fd, 'w') as status:
+ print('Generating config.status')
+ status.write('#!%s\n' % sys.executable)
+ status.write('import os\n')
+ status.write('import sys\n')
+ status.write('old_build_dir = os.path.dirname(sys.argv[0])\n')
+ status.write('configure = os.path.join(old_build_dir, %r, "configure")\n'
+ % self.relative_source())
+ seen_args = ['='.join(kv) for kv in self._seen_args.items()]
+ status.write('os.execvp(configure, [configure] + %r + sys.argv[1:])\n'
+ % seen_args)
+
+ def configure(self, args, env):
+ ''' First apply variables from the environment,
+ then call apply_arg() a bunch of times, then finish().
+ '''
+ for k in self.project.options:
+ if k != as_var(k):
+ continue
+ val = env.get(k)
+ if val is not None:
+ self._seen_args[k] = val
+ opt = self.project.options[k]
+ self.vars[as_var(k)] = opt.type(val)
+
+ for arg in args:
+ self.apply_arg(arg)
+
+ self.finish()
+
+ def relative_source(self):
+ ''' Return a relative path from the build tree to the source tree.
+ '''
+ srcdir = self.project.srcdir
+ builddir = self.builddir
+ if os.path.isabs(srcdir) or os.path.isabs(builddir):
+ return os.path.realpath(srcdir)
+ return os.path.relpath(os.path.realpath(srcdir),
+ os.path.realpath(builddir))