diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | attoconf/_version.py | 2 | ||||
-rw-r--r-- | attoconf/classy.py | 112 | ||||
-rw-r--r-- | attoconf/core.py | 26 | ||||
-rw-r--r-- | attoconf/help.py | 8 | ||||
-rw-r--r-- | attoconf/lib/__init__.py | 0 | ||||
-rw-r--r-- | attoconf/lib/arches.py | 68 | ||||
-rw-r--r-- | attoconf/lib/c.py | 268 | ||||
-rw-r--r-- | attoconf/lib/install.py | 320 | ||||
-rw-r--r-- | attoconf/lib/make.py | 94 | ||||
-rw-r--r-- | attoconf/tests/test_core.py | 12 | ||||
-rw-r--r-- | demo-project/Makefile.in | 9 | ||||
-rwxr-xr-x | demo-project/configure | 54 | ||||
-rw-r--r-- | demo-project/header.c | 16 | ||||
-rw-r--r-- | demo-project/hello.c | 25 | ||||
-rw-r--r-- | demo-project/hello.h | 22 | ||||
-rw-r--r-- | demo-project/main.c | 24 | ||||
-rwxr-xr-x | test-everything.sh | 9 |
18 files changed, 1044 insertions, 26 deletions
@@ -1 +1,2 @@ *.pyc +/demo-build/ diff --git a/attoconf/_version.py b/attoconf/_version.py index 8d9820c..1b6e204 100644 --- a/attoconf/_version.py +++ b/attoconf/_version.py @@ -7,7 +7,7 @@ major = 0 # Incremented for releases with compatible API additions. # This is the number that is usually incremented. -minor = 1 +minor = 2 # Incremented if there is a bugfix release. # Might not be contiguous. diff --git a/attoconf/classy.py b/attoconf/classy.py new file mode 100644 index 0000000..f390ebc --- /dev/null +++ b/attoconf/classy.py @@ -0,0 +1,112 @@ +# Copyright 2013 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 __future__ import print_function, division, absolute_import + +from .core import Project, Build + +# TODO: put this in a metaclass +# some of the init/jiggle logic would also be simplified by a metaclass +def add_slots(cls): + ''' A decorator that fixes __slots__ after multiple inheritance. + ''' + return type(cls.__name__, cls.__bases__, + dict(cls.__dict__, __slots__=cls.slots())) + +class ClassyProject(Project): + ''' A more convenient, objectish, way of setting up a project. + ''' + __slots__ = () + @classmethod + def slots(self): + ''' slots don't work too well with multiple inheritance, + so make the most-derived class create all the slots + ''' + return () + + def __init__(self, srcdir): + super(ClassyProject, self).__init__(srcdir) + + def jiggle(self): + self.general() + self.paths() + self.arches() + self.vars() + self.features() + self.packages() + + if 0: + self.tests() + self.post() + + def general(self): + ''' Registration hook for general options (usually unneeded). + ''' + self.add_help('General:', hidden=False) + self.add_alias('--help', ['--help=default'], + help='display standard help, then exit', hidden=False) + self.add_option('--help', init=None, + type=self.do_help, check=None, + help='display some kind of help', hidden=False, + help_var='KIND') + self.help.add_option('--help=hidden', + help='display help you should never ever ever care about', + hidden=False) + + def paths(self): + ''' Registration hook for path-related options. + (--prefix, --bindir, etc) + + Probably only used by attoconf.lib.install. + ''' + + def arches(self): + ''' Registration hook for arch-related options. + (--build, --host, and sometimes --target) + + Typically this changes the prefix of the compiler. + ''' + + def vars(self): + ''' Environment variables (usually capital, don't start with a --). + + Usually there is one or two of these for every program needed. + ''' + + def features(self): + ''' Customizations for this package (--enable-*). + ''' + + def packages(self): + ''' Settings related to dependencies (--with-*). + ''' + + if 0: # not sure if really needed + def tests(self): + ''' Late tests, but still before post. + + ''' + + def post(self): + ''' Special hook for things that need to be done at the very end. + + attoconf.post.make + ''' + + # okay, no more registration hooks + def build(self, bindir): + return Build(self, bindir) diff --git a/attoconf/core.py b/attoconf/core.py index 8b53209..38ba485 100644 --- a/attoconf/core.py +++ b/attoconf/core.py @@ -28,7 +28,7 @@ Option = namedtuple('Option', ['type', 'init']) class ArgumentError(Exception): pass def as_var(name): - return name.lstrip('-').upper() + return name.lstrip('-').replace('-', '_').upper() def trim_trailing_slashes(path): p, s = os.path.split(path) @@ -95,7 +95,9 @@ class Project(object): if name in self.options: raise KeyError(name) self.options[name] = Option(init=init, type=type) - self.checks.append(lambda bld: check(bld, bld.vars[as_var(name)][0])) + if check is not None: + self.checks.append( + lambda bld: check(bld, bld.vars[as_var(name)][0]) ) if help_var is None: help_var = as_var(name) if help_def is None: @@ -125,15 +127,15 @@ class Build(object): ''' A Build is a directory and set of options applied to a Project. ''' __slots__ = ( - 'bindir', + 'builddir', 'project', 'vars', ) - def __init__(self, project, bindir): + def __init__(self, project, builddir): ''' A Build is initially constructed from a project and a build dir. ''' self.project = project - self.bindir = trim_trailing_slashes(bindir) + self.builddir = trim_trailing_slashes(builddir) self.vars = {as_var(k): (o.init, 'default') for k, o in project.options.iteritems()} @@ -173,7 +175,9 @@ class Build(object): ''' First apply variables from the environment, then call apply_arg() a bunch of times, then finish(). ''' - for k in self.vars: + for k in self.project.options: + if k != as_var(k): + continue val = env.get(k) if val is not None: self.vars[k] = (self.project.options[k].type(val), 'environment') @@ -186,5 +190,11 @@ class Build(object): def relative_source(self): ''' Return a relative path from the build tree to the source tree. ''' - return os.path.relpath(os.path.realpath(self.project.srcdir), - os.path.realpath(self.bindir)) + srcdir = self.project.srcdir + builddir = self.builddir + if os.path.isabs(srcdir): + return srcdir + if os.path.isabs(builddir): + return os.path.realpath(srcdir) + return os.path.relpath(os.path.realpath(srcdir), + os.path.realpath(builddir)) diff --git a/attoconf/help.py b/attoconf/help.py index 610efde..0fbc0a3 100644 --- a/attoconf/help.py +++ b/attoconf/help.py @@ -140,23 +140,23 @@ class Help(object): ''' self.sections = [] - def add_text(self, text, hidden): + def add_text(self, help, hidden): ''' Add a header line. This creates a new section if the last line wasn't a header. ''' if not self.sections or self.sections[-1].options: self.sections.append(HelpSection()) - self.sections[-1].add_text(text, hidden) + self.sections[-1].add_text(help, hidden) - def add_option(self, name, text, hidden): + def add_option(self, name, help, hidden): ''' Add an option with its description to the current section. This creates a new section only if it is first. ''' if not self.sections: self.sections.append(HelpSection()) - self.sections[-1].add_option(name, text, hidden) + self.sections[-1].add_option(name, help, hidden) def print(self, file, hidden, width=0): ''' Print all the help at the given level of hidden-ness. diff --git a/attoconf/lib/__init__.py b/attoconf/lib/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/attoconf/lib/__init__.py diff --git a/attoconf/lib/arches.py b/attoconf/lib/arches.py new file mode 100644 index 0000000..bafef38 --- /dev/null +++ b/attoconf/lib/arches.py @@ -0,0 +1,68 @@ +# Copyright 2013 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 __future__ import print_function, division, absolute_import + +from ..classy import ClassyProject + +def triple(s): + # Triples do not, in fact, follow a regular pattern. + # Some have only two segments, some appear to have four ... + # Also, sometimes a wrong thing is used as a triple. + # All we *really* care about is generating the tool names. + if s.startswith('-') or s.endswith('-') or '-' not in s[1:-1]: + raise ValueError('Probably not a triple') + return s + +def host(build, HOST): + if HOST is None: + BUILD, origin = build.vars['BUILD'] + if origin != 'default': + origin = 'derived from BUILD' + build.vars['HOST'] = (BUILD, origin) + +def target(build, TARGET): + if TARGET is None: + HOST, origin = build.vars['HOST'] + if origin != 'default': + origin = 'derived from HOST' + build.vars['TARGET'] = (HOST, origin) + +class Arches2(ClassyProject): + __slots__ = () + + def arches(self): + super(Arches2, self).arches() + self.add_help('System types:', hidden=False) + self.add_option('--build', init=None, + type=triple, check=None, + help='configure for building on BUILD', hidden=False, + help_def='native') + self.add_option('--host', init=None, + type=triple, check=host, + help='cross-compile to build programs to run on HOST', + hidden=False, help_def='BUILD') + +# TODO figure out the mro implications when I use this +class Arches3(Arches2): + __slots__ = () + def arches(self): + super(Arches3, self).arches() + self.add_option('--target', init=None, + type=triple, check=target, + help='configure for building compilers for TARGET', + hidden=False, help_def='HOST') diff --git a/attoconf/lib/c.py b/attoconf/lib/c.py new file mode 100644 index 0000000..4d3f252 --- /dev/null +++ b/attoconf/lib/c.py @@ -0,0 +1,268 @@ +# Copyright 2013 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 __future__ import print_function, division, absolute_import + +import errno +import os +from shlex import split as shell +import subprocess + +from .arches import Arches2 + +class TestError(Exception): + pass + +def do_exec(build, args): + p = subprocess.Popen(args, cwd=build.builddir, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + out, _ = p.communicate() + retcode = p.wait() + return retcode, out + + +class TempFile: + ''' context manager that optionally creates and then removes a file + ''' + __slots__ = ('filename') + + def __init__(self, filename, content): + self.filename = filename + if content is not None: + with open(filename, 'wx') as of: + of.write(content) + else: + # TODO: raise OSError(errno.EEXIST) if file already exists + pass + + def __enter__(self): + pass + + def __exit__(self, type, value, traceback): + try: + os.remove(self.filename) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + +def try_compile_c(build, body, CFLAGS=[], CPPFLAGS=[]): + CC = build.vars['CC'][0] + CFLAGS = build.vars['CFLAGS'][0] + CFLAGS + CPPFLAGS = build.vars['CPPFLAGS'][0] + CPPFLAGS + in_ = 'atto-test.c' + ins = [in_] + out = 'atto-test.o' + + args = CC + CFLAGS + CPPFLAGS + ['-c', '-o', out, in_] + with TempFile(in_, body), TempFile(out, None): + status, error = do_exec(build, args) + if status: + raise TestError(error) + +def try_compile_link_c(build, body, CFLAGS=[], CPPFLAGS=[], LDFLAGS=[], LDLIBS=[]): + CC = build.vars['CC'][0] + CFLAGS = build.vars['CFLAGS'][0] + CFLAGS + CPPFLAGS = build.vars['CPPFLAGS'][0] + CPPFLAGS + LDFLAGS = build.vars['LDFLAGS'][0] + LDFLAGS + LDLIBS = build.vars['LDLIBS'][0] + LDLIBS + in_ = 'atto-test.c' + ins = [in_] + out = 'atto-test' + + args = CC + CFLAGS + CPPFLAGS + LDFLAGS + ins + LDLIBS + ['-o', out] + with TempFile(in_, body), TempFile(out, None): + status, error = do_exec(build, args) + if status: + raise TestError(error) + +def try_compile_cxx(build, body, CXXFLAGS=[], CPPFLAGS=[]): + CXX = build.vars['CXX'][0] + CXXFLAGS = build.vars['CXXFLAGS'][0] + CXXFLAGS + CPPFLAGS = build.vars['CPPFLAGS'][0] + CPPFLAGS + in_ = 'atto-test.cxx' + out = 'atto-test.o' + + args = CXX + CXXFLAGS + CPPFLAGS + ['-c', '-o', out, in_] + with TempFile(in_, body), TempFile(out, None): + status, error = do_exec(build, args) + if status: + raise TestError(error) + +def try_compile_link_cxx(build, body, CXXFLAGS=[], CPPFLAGS=[], LDFLAGS=[], LDLIBS=[]): + CXX = build.vars['CXX'][0] + CXXFLAGS = build.vars['CXXFLAGS'][0] + CXXFLAGS + CPPFLAGS = build.vars['CPPFLAGS'][0] + CPPFLAGS + LDFLAGS = build.vars['LDFLAGS'][0] + LDFLAGS + LDLIBS = build.vars['LDLIBS'][0] + LDLIBS + in_ = 'atto-test.cxx' + out = 'atto-test' + + args = CXX + CXXFLAGS + CPPFLAGS + LDFLAGS + ins + LDLIBS + ['-o', out] + with TempFile(in_, body), TempFile(out, None): + status, error = do_exec(build, args) + if status: + raise TestError(error) + +if 0: + def try_linkonly_c(build, ins, LDFLAGS=[], LDLIBS=[]): + CC = build.vars['CC'][0] + LDFLAGS = build.vars['LDFLAGS'][0] + LDFLAGS + LDLIBS = build.vars['LDLIBS'][0] + LDLIBS + out = 'atto-test' + + args = CC + LDFLAGS + ins + LDLIBS + ['-o', out] + with TempFile(out, None): + status, error = do_exec(build, args) + if status: + raise TestError(error) + +def try_compile_link2_c(build, body, CFLAGS=[], CPPFLAGS=[], LDFLAGS=[], LDLIBS=[]): + CC = build.vars['CC'][0] + CFLAGS = build.vars['CFLAGS'][0] + CFLAGS + CPPFLAGS = build.vars['CPPFLAGS'][0] + CPPFLAGS + LDFLAGS = build.vars['LDFLAGS'][0] + LDFLAGS + LDLIBS = build.vars['LDLIBS'][0] + LDLIBS + in_ = 'atto-test.c' + ins = [in_] + mid = 'atto-test.o' + mids = [mid] + out = 'atto-test' + + args1 = CC + CFLAGS + CPPFLAGS + ['-c', '-o', mid, in_] + args2 = CC + LDFLAGS + mids + LDLIBS + ['-o', out] + with TempFile(mid, None): + with TempFile(in_, body): + status, error = do_exec(build, args1) + if status: + raise TestError(error) + + with TempFile(out, None): + status, error = do_exec(build, args2) + if status: + raise TestError(error) + +if 0: + def try_linkonly_cxx(build, ins, LDFLAGS=[], LDLIBS=[]): + CXX = build.vars['CXX'][0] + LDFLAGS = build.vars['LDFLAGS'][0] + LDFLAGS + LDLIBS = build.vars['LDLIBS'][0] + LDLIBS + out = 'atto-test' + + args = CXX + LDFLAGS + ins + LDLIBS + ['-o', out] + with TempFile(out, None): + status, error = do_exec(build, args) + if status: + raise TestError(error) + +def try_compile_link2_cxx(build, body, CXXFLAGS=[], CPPFLAGS=[], LDFLAGS=[], LDLIBS=[]): + CXX = build.vars['CXX'][0] + CXXFLAGS = build.vars['CXXFLAGS'][0] + CXXFLAGS + CPPFLAGS = build.vars['CPPFLAGS'][0] + CPPFLAGS + LDFLAGS = build.vars['LDFLAGS'][0] + LDFLAGS + LDLIBS = build.vars['LDLIBS'][0] + LDLIBS + in_ = 'atto-test.cxx' + ins = [in_] + mid = 'atto-test.o' + mids = [mid] + out = 'atto-test' + + args1 = CXX + CXXFLAGS + CPPFLAGS + ['-c', '-o', mid, in_] + args2 = CXX + LDFLAGS + mids + LDLIBS + ['-o', out] + with TempFile(mid, None): + with TempFile(in_, body): + status, error = do_exec(build, args1) + if status: + raise TestError(error) + + with TempFile(out, None): + status, error = do_exec(build, args2) + if status: + raise TestError(error) + + +def ldflags(build, LDFLAGS): + pass + +def libs(build, LIBS): + # Make expects something different + build.vars['LDLIBS'] = build.vars['LIBS'] + del build.vars['LIBS'] + +def cppflags(build, CPPFLAGS): + pass + +def cc(build, CC): + pass + +def cflags(build, CFLAGS): + try_compile_c(build, 'int main() {}\n') + try_compile_link_c(build, 'int main() {}\n') + try_compile_link2_c(build, 'int main() {}\n') + +def cxx(build, CXX): + pass + +def cxxflags(build, CXXFLAGS): + try_compile_cxx(build, 'int main() {}\n') + try_compile_link_cxx(build, 'int main() {}\n') + try_compile_link2_cxx(build, 'int main() {}\n') + +class Link(Arches2): + __slots__ = () + def vars(self): + super(Link, self).vars() + self.add_option('LDFLAGS', init=[], + type=shell, check=ldflags, + help='linker flags, e.g. -L<lib dir> if you have libraries in a nonstandard directory <lib dir>', + hidden=False) + self.add_option('LIBS', init=[], + type=shell, check=libs, + help='libraries to pass to the linker, e.g. -l<library>', + hidden=False) + +class Preprocess(Arches2): + __slots__ = () + def vars(self): + super(Preprocess, self).vars() + self.add_option('CPPFLAGS', init=[], + type=shell, check=cppflags, + help='C/C++/Objective C preprocessor flags, e.g. -I<include dir> if you have headers in a nonstandard directory <include dir>', + hidden=False) + +class C(Link, Preprocess): + __slots__ = () + def vars(self): + super(C, self).vars() + self.add_option('CC', init=['gcc'], + type=shell, check=cc, + help='C compiler command', hidden=False) + self.add_option('CFLAGS', init=[], + type=shell, check=cflags, + help='C compiler flags', hidden=False) + +class Cxx(Link, Preprocess): + __slots__ = () + def vars(self): + super(Cxx, self).vars() + self.add_option('CXX', init=['g++'], + type=shell, check=cxx, + help='C++ compiler command', hidden=False) + self.add_option('CXXFLAGS', init=[], + type=shell, check=cxxflags, + help='C++ compiler flags', hidden=False) diff --git a/attoconf/lib/install.py b/attoconf/lib/install.py new file mode 100644 index 0000000..3e135e8 --- /dev/null +++ b/attoconf/lib/install.py @@ -0,0 +1,320 @@ +# Copyright 2013 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 __future__ import print_function, division, absolute_import + +import os + +from ..classy import ClassyProject +from ..core import trim_trailing_slashes + + +def word(s): + if ' ' in s: + raise ValueError('not a word: %s' % s) + return s + + +def version(s): + if s.startswith('v'): + s = s[1:] + [int(b) for b in s.split('.')] + return s + + +def filepath(s): + s = trim_trailing_slashes(s) + # must be absolute *and* canonical + if s != os.path.abspath(s): + raise ValueError('Not an absolute, canonical pathname: %s' % s) + return s + + +def exec_prefix(build, EPREFIX): + if EPREFIX is None: + PREFIX, origin = build.vars['PREFIX'] + if origin != 'default': + origin = 'derived from PREFIX' + build.vars['EXEC_PREFIX'] = (PREFIX, origin) + # is this a good idea? is there a better way? + # how will this interfere with hashing? + # at least we don't have to worry about the environment ... + build.vars['EPREFIX'] = build.vars['EXEC_PREFIX'] + del build.vars['EXEC_PREFIX'] + +def bindir(build, DIR): + if DIR is None: + EPREFIX, origin = build.vars['EPREFIX'] + if origin != 'default': + origin = 'derived from EPREFIX' + build.vars['BINDIR'] = (os.path.join(EPREFIX, 'bin'), origin) + +def sbindir(build, DIR): + if DIR is None: + EPREFIX, origin = build.vars['EPREFIX'] + if origin != 'default': + origin = 'derived from EPREFIX' + build.vars['SBINDIR'] = (os.path.join(EPREFIX, 'sbin'), origin) + +def libexecdir(build, DIR): + if DIR is None: + EPREFIX, origin = build.vars['EPREFIX'] + if origin != 'default': + origin = 'derived from EPREFIX' + build.vars['LIBEXECDIR'] = (os.path.join(EPREFIX, 'libexec'), origin) + +def sysconfdir(build, DIR): + if DIR is None: + PREFIX, origin = build.vars['PREFIX'] + if origin != 'default': + origin = 'derived from PREFIX' + build.vars['SYSCONFDIR'] = (os.path.join(PREFIX, 'etc'), origin) + +def sharedstatedir(build, DIR): + if DIR is None: + PREFIX, origin = build.vars['PREFIX'] + if origin != 'default': + origin = 'derived from PREFIX' + build.vars['SHAREDSTATEDIR'] = (os.path.join(PREFIX, 'com'), origin) + +def localstatedir(build, DIR): + if DIR is None: + PREFIX, origin = build.vars['PREFIX'] + if origin != 'default': + origin = 'derived from PREFIX' + build.vars['LOCALSTATEDIR'] = (os.path.join(PREFIX, 'var'), origin) + +def libdir(build, DIR): + if DIR is None: + EPREFIX, origin = build.vars['EPREFIX'] + if origin != 'default': + origin = 'derived from EPREFIX' + build.vars['LIBDIR'] = (os.path.join(EPREFIX, 'lib'), origin) + +def includedir(build, DIR): + if DIR is None: + PREFIX, origin = build.vars['PREFIX'] + if origin != 'default': + origin = 'derived from PREFIX' + build.vars['INCLUDEDIR'] = (os.path.join(PREFIX, 'include'), origin) + +def datarootdir(build, DIR): + if DIR is None: + PREFIX, origin = build.vars['PREFIX'] + if origin != 'default': + origin = 'derived from PREFIX' + build.vars['DATAROOTDIR'] = (os.path.join(PREFIX, 'share'), origin) + +def datadir(build, DIR): + if DIR is None: + DATAROOTDIR, origin = build.vars['DATAROOTDIR'] + if origin != 'default': + origin = 'derived from DATAROOTDIR' + build.vars['DATADIR'] = (DATAROOTDIR, origin) + +def packagedatadir(build, DIR): + if DIR is None: + DATADIR, origin = build.vars['DATADIR'] + PACKAGE, prigin = build.vars['PACKAGE'] + if origin != 'default' or prigin != 'default': + origin = 'derived from DATADIR and PACKAGE' + build.vars['DATADIR'] = (os.path.join(DATADIR, PACKAGE), origin) + +def infodir(build, DIR): + if DIR is None: + DATAROOTDIR, origin = build.vars['DATAROOTDIR'] + if origin != 'default': + origin = 'derived from DATAROOTDIR' + build.vars['INFODIR'] = (os.path.join(DATAROOTDIR, 'info'), origin) + +def localedir(build, DIR): + if DIR is None: + DATAROOTDIR, origin = build.vars['DATAROOTDIR'] + if origin != 'default': + origin = 'derived from DATAROOTDIR' + build.vars['LOCALEDIR'] = (os.path.join(DATAROOTDIR, 'locale'), origin) + +def mandir(build, DIR): + if DIR is None: + DATAROOTDIR, origin = build.vars['DATAROOTDIR'] + if origin != 'default': + origin = 'derived from DATAROOTDIR' + build.vars['MANDIR'] = (os.path.join(DATAROOTDIR, 'man'), origin) + +def docdir(build, DIR): + if DIR is None: + DATAROOTDIR, origin = build.vars['DATAROOTDIR'] + PACKAGE, origin2 = build.vars['PACKAGE'] + if origin != 'default' or origin2 != 'default': + origin = 'derived from DATAROOTDIR and PACKAGE' + build.vars['DOCDIR'] = (os.path.join(DATAROOTDIR, 'doc', PACKAGE), origin) + +def htmldir(build, DIR): + if DIR is None: + DOCDIR, origin = build.vars['DOCDIR'] + if origin != 'default': + origin = 'derived from DOCDIR' + build.vars['HTMLDIR'] = (DOCDIR, origin) + +def dvidir(build, DIR): + if DIR is None: + DOCDIR, origin = build.vars['DOCDIR'] + if origin != 'default': + origin = 'derived from DOCDIR' + build.vars['DVIDIR'] = (DOCDIR, origin) + +def pdfdir(build, DIR): + if DIR is None: + DOCDIR, origin = build.vars['DOCDIR'] + if origin != 'default': + origin = 'derived from DOCDIR' + build.vars['PDFDIR'] = (DOCDIR, origin) + +def psdir(build, DIR): + if DIR is None: + DOCDIR, origin = build.vars['DOCDIR'] + if origin != 'default': + origin = 'derived from DOCDIR' + build.vars['PSDIR'] = (DOCDIR, origin) + + +class Install(ClassyProject): + __slots__ = () + + @classmethod + def slots(cls): + return super(Install, cls).slots() + ( + 'package', 'package_version', 'package_name') + + def set_package(self, package, version, name): + self.package = package + self.package_version = version + self.package_name = name + + def general(self): + super(Install, self).general() + self.add_option('--package', init=self.package, + type=word, check=None, + help='Short name of this package (don\'t change!)', + hidden=True) + self.add_option('--package-version', init=self.package_version, + type=version, check=None, + help='Version of this package (change in configure)', + hidden=True, + help_var='VERSION') + self.add_option('--package-name', init=self.package_name, + type=version, check=None, + help='Long name of this package (don\'t change)', + hidden=True, + help_var='NAME') + + def paths(self): + super(Install, self).paths() + + self.add_help('Installation directories:', hidden=False) + self.add_option('--prefix', init='/usr/local', + type=filepath, check=None, + help='install architecture-independent files in PREFIX', + hidden=False) + self.add_option('--exec-prefix', init=None, + type=filepath, check=exec_prefix, + help='install architecture-dependent files in EPREFIX', + hidden=False, + help_var='EPREFIX', help_def='PREFIX') + + self.add_help('Fine tuning of the installation directories:', + hidden=False) + self.add_option('--bindir', init=None, + type=filepath, check=bindir, + help='user executables', hidden=False, + help_var='DIR', help_def='EPREFIX/bin') + self.add_option('--sbindir', init=None, + type=filepath, check=sbindir, + help='system admin executables', hidden=False, + help_var='DIR', help_def='EPREFIX/sbin') + self.add_option('--libexecdir', init=None, + type=filepath, check=libexecdir, + help='program executables', hidden=False, + help_var='DIR', help_def='EPREFIX/libexec') + self.add_option('--sysconfdir', init=None, + type=filepath, check=sysconfdir, + help='read-only single-machine data', hidden=False, + help_var='DIR', help_def='PREFIX/etc') + self.add_option('--sharedstatedir', init=None, + type=filepath, check=sharedstatedir, + help='modifiable architecture-independent data', hidden=False, + help_var='DIR', help_def='PREFIX/com') + self.add_option('--localstatedir', init=None, + type=filepath, check=localstatedir, + help='modifiable single-machine data', hidden=False, + help_var='DIR', help_def='PREFIX/var') + self.add_option('--libdir', init=None, + type=filepath, check=libdir, + help='object code libraries', hidden=False, + help_var='DIR', help_def='EPREFIX/lib') + self.add_option('--includedir', init=None, + type=filepath, check=includedir, + help='C header files', hidden=False, + help_var='DIR', help_def='PREFIX/include') + self.add_option('--oldincludedir', init='/usr/include', + type=filepath, check=None, + help='C header files for non-gcc', hidden=False, + help_var='DIR') + self.add_option('--datarootdir', init=None, + type=filepath, check=datarootdir, + help='read-only arch.-independent data root', hidden=False, + help_var='DIR', help_def='PREFIX/share') + self.add_option('--datadir', init=None, + type=filepath, check=datadir, + help='read-only architecture-independent data', hidden=False, + help_var='DIR', help_def='DATAROOTDIR') + self.add_option('--packagedatadir', init=None, + type=filepath, check=packagedatadir, + help='data specific to this package (please set datadir instead)', hidden=False, + help_var='DIR', help_def='DATADIR/PACKAGE') + self.add_option('--infodir', init=None, + type=filepath, check=infodir, + help='info documentation', hidden=False, + help_var='DIR', help_def='DATAROOTDIR/info') + self.add_option('--localedir', init=None, + type=filepath, check=localedir, + help='locale-dependent data', hidden=False, + help_var='DIR', help_def='DATAROOTDIR/locale') + self.add_option('--mandir', init=None, + type=filepath, check=mandir, + help='man documentation', hidden=False, + help_var='DIR', help_def='DATAROOTDIR/man') + self.add_option('--docdir', init=None, + type=filepath, check=docdir, + help='documentation root', hidden=False, + help_var='DIR', help_def='DATAROOTDIR/doc/PACKAGE') + self.add_option('--htmldir', init=None, + type=filepath, check=htmldir, + help='html documentation', hidden=False, + help_var='DIR', help_def='DOCDIR') + self.add_option('--dvidir', init=None, + type=filepath, check=dvidir, + help='dvi documentation', hidden=False, + help_var='DIR', help_def='DOCDIR') + self.add_option('--pdfdir', init=None, + type=filepath, check=pdfdir, + help='pdf documentation', hidden=False, + help_var='DIR', help_def='DOCDIR') + self.add_option('--psdir', init=None, + type=filepath, check=psdir, + help='ps documentation', hidden=False, + help_var='DIR', help_def='DOCDIR') diff --git a/attoconf/lib/make.py b/attoconf/lib/make.py new file mode 100644 index 0000000..e54224d --- /dev/null +++ b/attoconf/lib/make.py @@ -0,0 +1,94 @@ +# Copyright 2013 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 __future__ import print_function, division, absolute_import + +import os +from pipes import quote + +from ..classy import ClassyProject +from ..version import string as version_string + +blacklist = frozenset(''.join(chr(i) for i in range(0x20)) + '#$') +def validate(s): + return s == s.strip() and not frozenset(s) & blacklist + + +class MakeHook(object): + __slots__ = ('infile', 'outfile') + def __init__(self, infile, outfile): + self.infile = infile + self.outfile = outfile + + def __call__(self, build): + if self.outfile is None: + # if there are multiple backends + print('Skipping generation of a makefile') + return + with open(os.path.join(build.builddir, self.outfile), 'w') as out: + print('Generating a makefile ...') + out.write('# This part was generated by %s\n' % version_string) + out.write('SRC_DIR = %s\n' % build.relative_source()) + out.write('\n') + # TODO preserve *original* order? + for var, (val, origin) in sorted(build.vars.iteritems()): + if val is None: + if origin == 'default': + continue + # is it a good idea for Nones to survive this long? + # especially conditional ones ... + var = '# ' + var + val = 'not defined' + elif isinstance(val, list): + val = ' '.join(quote(a) for a in val) + elif isinstance(val, str): + val = quote(val) + else: + print('Assuming it\'s safe to print an instance of', + type(val).__name__, '...') + out.write('%s = %s # %s\n' % (var, val, origin)) + if self.infile is not None: + out.write('\n# The rest was copied from %s\n' % self.infile) + infile = os.path.join(build.project.srcdir, self.infile) + with open(infile) as in_: + for line in in_: + assert line.endswith('\n') + out.write(line) + + +class Make(ClassyProject): + ''' Post hook to generate a Makefile from Makefile.in + ''' + __slots__ = () + @classmethod + def slots(cls): + return super(Make, cls).slots() + ('make_in', 'make_out') + + def __init__(self, srcdir): + super(Make, self).__init__(srcdir) + self.set_make_infile('Makefile.in') + self.set_make_outfile('Makefile') # relative to build dir + + def set_make_infile(self, ipath): + self.make_in = ipath + + def set_make_outfile(self, opath): + self.make_out = opath + + def post(self): + super(Make, self).post() + self.checks.append(MakeHook(self.make_in, self.make_out)) diff --git a/attoconf/tests/test_core.py b/attoconf/tests/test_core.py index 88bf616..f9854e8 100644 --- a/attoconf/tests/test_core.py +++ b/attoconf/tests/test_core.py @@ -47,7 +47,8 @@ class TestProject(unittest.TestCase): help='display some subset of help', hidden=False, help_var='TYPE') proj.help.add_option('--help=hidden', - 'display help you should never ever ever care about', True) + help='display help you should never ever ever care about', + hidden=True) proj.add_option('--foo', init='asdf', type=str, check=None, help='set frob target', hidden=False) @@ -89,7 +90,7 @@ General: proj = Project('foo/') build = Build(proj, 'bar/') self.assertEquals(build.project.srcdir, 'foo') - self.assertEquals(build.bindir, 'bar') + self.assertEquals(build.builddir, 'bar') self.assertEquals(build.relative_source(), '../foo') def test_configure(self): @@ -119,7 +120,12 @@ General: help='help for string VAR', hidden=False) build = Build(proj, '.') - build.configure(['--alias'], {'VAR': 'value'}) + build.configure(['--alias'], + { + 'VAR': 'value', + 'QUX': 'a', + '--qux': 'b', + }) self.assertEqual(build.vars, { 'FOO': ('B', 'command-line'), diff --git a/demo-project/Makefile.in b/demo-project/Makefile.in new file mode 100644 index 0000000..8f3f4f0 --- /dev/null +++ b/demo-project/Makefile.in @@ -0,0 +1,9 @@ +vpath %.c ${SRC_DIR} +vpath %.h ${SRC_DIR} + +hello: main.o hello.o +main.o: main.c hello.h +hello.o: hello.c hello.h + +clean: + rm -f hello main.o hello.o diff --git a/demo-project/configure b/demo-project/configure index dc29870..a3e49ee 100755 --- a/demo-project/configure +++ b/demo-project/configure @@ -23,17 +23,53 @@ import os import sys from attoconf.version import require_version, string as version_string -require_version(0, 1) +require_version(0, 2) -from attoconf.core import Project, Build +from attoconf.classy import add_slots +from attoconf.lib.c import C +from attoconf.lib.install import Install +from attoconf.lib.make import Make -def main(): - print('Using', version_string) - src = Project(os.path.dirname(sys.argv[0])) - # customizations go here +@add_slots +class Configuration(C, Install, Make): + # usually you'll only have vars, features, and packages + # the rest should only be inherited + def __init__(self, srcdir): + super(Configuration, self).__init__(srcdir) + + def general(self): + super(Configuration, self).general() + + def paths(self): + super(Configuration, self).paths() + + def arches(self): + super(Configuration, self).arches() + + def vars(self): + super(Configuration, self).vars() + + def features(self): + super(Configuration, self).features() - # the rest shouldn't change - build = Build(src, '.') + def packages(self): + super(Configuration, self).packages() + + if 0: + def tests(self): + super(Configuration, self).tests() + + def post(self): + super(Configuration, self).post() + + +def main(): + proj = Configuration(os.path.dirname(sys.argv[0])) + proj.set_package('attoconf-demo', version_string.split(' ')[1], 'Demo project for attoconf') + proj.jiggle() + build = proj.build('.') build.configure(sys.argv[1:], os.environ) -main() + +if __name__ == '__main__': + main() diff --git a/demo-project/header.c b/demo-project/header.c new file mode 100644 index 0000000..f231086 --- /dev/null +++ b/demo-project/header.c @@ -0,0 +1,16 @@ +// Copyright 2013 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/>. diff --git a/demo-project/hello.c b/demo-project/hello.c new file mode 100644 index 0000000..b018dc3 --- /dev/null +++ b/demo-project/hello.c @@ -0,0 +1,25 @@ +// Copyright 2013 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/>. + +#include "hello.h" + +#include <stdio.h> + +void hello(void) +{ + puts("Hello, World!"); +} diff --git a/demo-project/hello.h b/demo-project/hello.h new file mode 100644 index 0000000..899b9cc --- /dev/null +++ b/demo-project/hello.h @@ -0,0 +1,22 @@ +#ifndef HELLO_H +#define HELLO_H +// Copyright 2013 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/>. + +void hello(void); + +#endif // HELLO_H diff --git a/demo-project/main.c b/demo-project/main.c new file mode 100644 index 0000000..2d363e2 --- /dev/null +++ b/demo-project/main.c @@ -0,0 +1,24 @@ +// Copyright 2013 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/>. + +#include "hello.h" + +int main() +{ + hello(); + return 0; +} diff --git a/test-everything.sh b/test-everything.sh index e16866b..64fc89d 100755 --- a/test-everything.sh +++ b/test-everything.sh @@ -1,2 +1,9 @@ -#!/bin/sh +#!/bin/sh -e python -m unittest discover "$@" +mkdir -p demo-build +cd demo-build +../demo-project/configure +make +./hello +cd .. +rm -r demo-build |