Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New AutotoolsDeps, AutotoolsToolchain helpers in conan.tools.gnu #8457

Merged
merged 64 commits into from Mar 29, 2021
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
a841c9b
refactor autotools tests
memsharded Jan 21, 2021
062c3f9
autotools
memsharded Jan 21, 2021
7330385
Merge branch 'develop' into feature/tools_autotools
memsharded Jan 21, 2021
a92a2cb
working
memsharded Jan 22, 2021
08e7144
Merge branch 'develop' into feature/tools_autotools
memsharded Jan 28, 2021
e41792c
working
memsharded Jan 28, 2021
03a4bbf
working .bat
memsharded Jan 30, 2021
74e9198
Merge branch 'develop' into feature/environment
memsharded Jan 31, 2021
3292aec
working in Windows and Linux
memsharded Feb 1, 2021
087f030
Merge branch 'feature/environment' into feature/tools_autotools
memsharded Feb 1, 2021
fd18ee9
test passing Linux
memsharded Feb 1, 2021
b33cd92
working
memsharded Feb 1, 2021
cc5c8b3
Merge branch 'feature/environment' into feature/tools_autotools
memsharded Feb 1, 2021
9b997f8
working
memsharded Feb 1, 2021
cf97406
improved environment
memsharded Feb 2, 2021
79675af
Merge branch 'develop' into feature/tools_autotools
memsharded Feb 2, 2021
dfda73e
Merge branch 'feature/environment' into feature/tools_autotools
memsharded Feb 2, 2021
c6226b6
mingw test
memsharded Feb 2, 2021
c2b7e63
working
memsharded Feb 5, 2021
36bf481
Merge branch 'develop' into feature/tools_autotools
memsharded Feb 5, 2021
642b3fe
working
memsharded Feb 5, 2021
38b4dd3
spaces
memsharded Feb 5, 2021
96d0334
working
memsharded Feb 8, 2021
6fe2b2f
Merge branch 'develop' into feature/tools_autotools
memsharded Feb 9, 2021
49cb4da
working
memsharded Feb 9, 2021
0b62989
refactoring architecture flag
memsharded Feb 9, 2021
d2de553
Merge branch 'refactor/architecture_flag' into feature/tools_autotools
memsharded Feb 9, 2021
837ab8f
working
memsharded Feb 9, 2021
ccdd17c
solve conflicts
memsharded Feb 9, 2021
4f3642c
fix test
memsharded Feb 9, 2021
14c308a
Merge branch 'develop' into feature/tools_autotools
memsharded Feb 11, 2021
6d4ea51
Merge branch 'develop' into feature/tools_autotools
memsharded Feb 11, 2021
ff0543e
Windows subsystems tests
memsharded Feb 12, 2021
bcd08d1
remove cmake test_package
memsharded Feb 12, 2021
35ced48
path wihtout spaces
memsharded Feb 12, 2021
c512f37
change mingw to msys one
memsharded Feb 12, 2021
9ec6582
trying again
memsharded Feb 12, 2021
d5d238e
fix adjust path
memsharded Feb 12, 2021
75ff512
proposal
memsharded Feb 21, 2021
99f3f91
Merge branch 'develop' into feature/tools_autotools
memsharded Feb 22, 2021
3a585e5
build-host contexts poc
memsharded Feb 23, 2021
5f323b1
added CMakeGen integration
memsharded Feb 24, 2021
16863a1
fixing Macos tests
memsharded Feb 24, 2021
3fd4633
review
memsharded Feb 24, 2021
de6787e
make sure profile compose and include() correctly
memsharded Feb 24, 2021
7f86beb
Merge branch 'develop' into feature/environment_propagate
memsharded Feb 24, 2021
49dfb47
Merge branch 'develop' into feature/tools_autotools
memsharded Feb 24, 2021
7d98f08
merging environment
memsharded Feb 24, 2021
7507507
working
memsharded Feb 24, 2021
f77726b
merged develop
memsharded Mar 24, 2021
e4e280e
reviewing merge
memsharded Mar 24, 2021
ab0375f
merge review
memsharded Mar 24, 2021
b4fbb64
Merge branch 'develop' into feature/tools_autotools
memsharded Mar 24, 2021
99a3ae9
adding diamond visits and avoid repetitions
memsharded Mar 25, 2021
d3f065f
Merge branch 'develop' into feature/tools_autotools
memsharded Mar 25, 2021
0fe4307
Merge branch 'fix/environment_diamond_visit' into feature/tools_autot…
memsharded Mar 25, 2021
59800d8
test working again
memsharded Mar 25, 2021
b1de1f4
working
memsharded Mar 25, 2021
5aee77f
Merge branch 'develop' into feature/tools_autotools
memsharded Mar 25, 2021
42c4dfc
test passing
memsharded Mar 25, 2021
fba1ede
working
memsharded Mar 25, 2021
e0e9c9c
merged develop
memsharded Mar 26, 2021
58776e5
fix conan.tools.CMake for Msys/Cygwin
memsharded Mar 27, 2021
67d2275
use correct make instead of mingw32-make in subsystems
memsharded Mar 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Expand Up @@ -7,7 +7,6 @@ __pycache__/

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
Expand Down
1 change: 1 addition & 0 deletions conan/tools/env/__init__.py
@@ -0,0 +1 @@
from conan.tools.env.environment import Environment
138 changes: 138 additions & 0 deletions conan/tools/env/environment.py
@@ -0,0 +1,138 @@
import textwrap
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this one deserves its own first class generator? and new Virtual*Env generators might be based on that one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet. This could be a first class model, like self.env_info = Environment() in the recipes. This is separated in another branch and will have a separate PR once it is stabilized.

from collections import OrderedDict

from conans.util.files import save

PLACEHOLDER = "$CONANVARPLACEHOLDER%"


class EnvironmentItem:

def __init__(self, value=None, separator=None):
self._value = value
self._separator = separator

def value(self, placeholder):
value = [v if v != PLACEHOLDER else placeholder for v in self._value]
value = self._separator.join(value) if value else ""
return value

def copy(self):
return EnvironmentItem(self._value[:], self._separator)

def define(self, value, separator=" "):
self._value = value if isinstance(value, list) else [value]
self._separator = separator

def append(self, value, separator=" "):
value = value if isinstance(value, list) else [value]
self._value = [PLACEHOLDER] + value
self._separator = separator

def prepend(self, value, separator=" "):
value = value if isinstance(value, list) else [value]
self._value = value + [PLACEHOLDER]
self._separator = separator

def clean(self):
self._value = []
self._separator = None

def update(self, other):
"""
:type other: EnvironmentItem
"""
result = other._value
try:
index = result.index(PLACEHOLDER)
result[index:index+1] = self._value
assert self._separator == other._separator
except ValueError:
pass
self._value = result
self._separator = other._separator


class Environment:
def __init__(self):
# It being ordered allows for Windows case-insensitive composition
self._values = OrderedDict()

def __getitem__(self, name):
return self._values.setdefault(name, EnvironmentItem())

def save_bat(self, filename, generate_deactivate=True):
deactivate = textwrap.dedent("""\
echo Capturing current environment in deactivate_{filename}
setlocal
echo @echo off > "deactivate_{filename}"
echo echo Restoring environment >> "deactivate_{filename}"
for %%v in ({vars}) do (
set foundenvvar=
for /f "delims== tokens=1,2" %%a in ('set') do (
if "%%a" == "%%v" (
echo set %%a=%%b>> "deactivate_{filename}"
set foundenvvar=1
)
)
if not defined foundenvvar (
echo set %%v=>> "deactivate_{filename}"
)
)
endlocal

""").format(filename=filename, vars=" ".join(self._values.keys()))
capture = textwrap.dedent("""\
@echo off
{deactivate}
echo Configuring environment variables
""").format(deactivate=deactivate if generate_deactivate else "")
result = [capture]
for k, v in self._values.items():
value = v.value("%{}%".format(k))
result.append('set {}={}'.format(k, value))

content = "\n".join(result)
save(filename, content)

def save_sh(self, filename):
capture = textwrap.dedent("""\
echo Capturing current environment in deactivate_{filename}
echo echo Restoring variables >> deactivate_{filename}
for v in {vars}
do
value=${{!v}}
if [ -n "$value" ]
then
echo export "$v=$value" >> deactivate_{filename}
else
echo unset $v >> deactivate_{filename}
fi
done
echo Configuring environment variables
""".format(filename=filename, vars=" ".join(self._values.keys())))
result = [capture]
for k, v in self._values.items():
value = v.value("${}".format(k))
if value:
result.append('export {}="{}"'.format(k, value))
else:
result.append('unset {}'.format(k))

content = "\n".join(result)
save(filename, content)

def compose(self, other):
"""
:type other: Environment
"""
result = Environment()
result._values = OrderedDict([(k, v.copy()) for k, v in self._values.items()])
for k, v in other._values.items():
v = v.copy()
existing = result._values.get(k)
if existing is None:
result._values[k] = v
else:
existing.update(v)
return result
3 changes: 3 additions & 0 deletions conan/tools/gnu/__init__.py
@@ -1 +1,4 @@
from .make import MakeToolchain
from conan.tools.gnu.autotoolstoolchain import AutotoolsToolchain
from conan.tools.gnu.autotoolsdeps import AutotoolsDeps
from conan.tools.gnu.autotools import Autotools
239 changes: 239 additions & 0 deletions conan/tools/gnu/autotools.py
@@ -0,0 +1,239 @@
import copy
import os
import platform


from conans.client import tools
from conans.client.tools.oss import OSInfo, cross_building, \
detected_architecture, detected_os, get_gnu_triplet, get_target_os_arch, get_build_os_arch
from conans.client.tools.win import unix_path
from conans.errors import ConanException
from conans.model.build_info import DEFAULT_BIN, DEFAULT_INCLUDE, DEFAULT_LIB, DEFAULT_SHARE
from conans.util.files import get_abs_path


class Autotools(object):

def __init__(self, conanfile, win_bash=False, include_rpath_flags=False):
"""
FIXME: include_rpath_flags CONAN 2.0 to default True? Could break many packages in center
"""
self._conanfile = conanfile
self._win_bash = win_bash
self._include_rpath_flags = include_rpath_flags
self.subsystem = OSInfo().detect_windows_subsystem() if self._win_bash else None
self._os = conanfile.settings.get_safe("os")
self._os_version = conanfile.settings.get_safe("os.version")
self._os_sdk = conanfile.settings.get_safe("os.sdk")
self._os_subsystem = conanfile.settings.get_safe("os.subsystem")
self._arch = conanfile.settings.get_safe("arch")
self._os_target, self._arch_target = get_target_os_arch(conanfile)
self._build_type = conanfile.settings.get_safe("build_type")
self._compiler = conanfile.settings.get_safe("compiler")
self._compiler_version = conanfile.settings.get_safe("compiler.version")
self._compiler_runtime = conanfile.settings.get_safe("compiler.runtime")

# Precalculate build, host, target triplets
self.build, self.host, self.target = self._get_host_build_target_flags()

def _get_host_build_target_flags(self):
"""Based on google search for build/host triplets, it could need a lot
and complex verification"""

if self._os_target and self._arch_target:
try:
target = get_gnu_triplet(self._os_target, self._arch_target, self._compiler)
except ConanException as exc:
self._conanfile.output.warn(str(exc))
target = None
else:
target = None

if hasattr(self._conanfile, 'settings_build'):
os_build, arch_build = get_build_os_arch(self._conanfile)
else:
# FIXME: Why not use 'os_build' and 'arch_build' from conanfile.settings?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's new helper, can we avoid all these os_build/arch_build mess and just use modern two-profile approach?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, definitely, it is very ongoing work, I plan to refactor everything and use the 2 profiles.

os_build = detected_os() or platform.system()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say, that may be going forward, avoid auto-detecting altogether and specify everything explicitly in profile/settings, to ensure it's reproducible?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100% agree, there will be no auto-detection here, only deterministic from profiles.

arch_build = detected_architecture() or platform.machine()

if os_build is None or arch_build is None or self._arch is None or self._os is None:
return False, False, target

if not cross_building(self._conanfile, os_build, arch_build):
return False, False, target

try:
build = get_gnu_triplet(os_build, arch_build, self._compiler)
except ConanException as exc:
self._conanfile.output.warn(str(exc))
build = None
try:
host = get_gnu_triplet(self._os, self._arch, self._compiler)
except ConanException as exc:
self._conanfile.output.warn(str(exc))
host = None
return build, host, target

def configure(self, configure_dir=None, args=None, build=None, host=None, target=None,
pkg_config_paths=None, vars=None, use_default_install_dirs=True):
"""
http://jingfenghanmax.blogspot.com.es/2010/09/configure-with-host-target-and-build.html
https://gcc.gnu.org/onlinedocs/gccint/Configure-Terms.html
:param use_default_install_dirs: Use or not the defaulted installation dirs

"""
if not self._conanfile.should_configure:
return
if configure_dir:
configure_dir = configure_dir.rstrip("/")
else:
configure_dir = "."
"""
triplet_args = []

if build is not False: # Skipped by user
if build or self.build: # User specified value or automatic
triplet_args.append("--build=%s" % (build or self.build))

if host is not False: # Skipped by user
if host or self.host: # User specified value or automatic
triplet_args.append("--host=%s" % (host or self.host))

if target is not False: # Skipped by user
if target or self.target: # User specified value or automatic
triplet_args.append("--target=%s" % (target or self.target))

if pkg_config_paths:
pkg_env = {"PKG_CONFIG_PATH":
[os.pathsep.join(get_abs_path(f, self._conanfile.install_folder)
for f in pkg_config_paths)]}
else:
# If we are using pkg_config generator automate the pcs location, otherwise it could
# read wrong files
pkg_env = {"PKG_CONFIG_PATH": [self._conanfile.install_folder]} \
if "pkg_config" in self._conanfile.generators else None
"""
configure_dir = self._adjust_path(configure_dir)

"""if self._conanfile.package_folder is not None:
if not args:
args = ["--prefix=%s" % self._conanfile.package_folder.replace("\\", "/")]
elif not self._is_flag_in_args("prefix", args):
args.append("--prefix=%s" % self._conanfile.package_folder.replace("\\", "/"))

all_flags = ["bindir", "sbindir", "libexecdir", "libdir", "includedir", "oldincludedir",
"datarootdir"]
help_output = self._configure_help_output(configure_dir)
available_flags = [flag for flag in all_flags if "--%s" % flag in help_output]

if use_default_install_dirs:
for varname in ["bindir", "sbindir", "libexecdir"]:
if self._valid_configure_flag(varname, args, available_flags):
args.append("--%s=${prefix}/%s" % (varname, DEFAULT_BIN))
if self._valid_configure_flag("libdir", args, available_flags):
args.append("--libdir=${prefix}/%s" % DEFAULT_LIB)
for varname in ["includedir", "oldincludedir"]:
if self._valid_configure_flag(varname, args, available_flags):
args.append("--%s=${prefix}/%s" % (varname, DEFAULT_INCLUDE))
if self._valid_configure_flag("datarootdir", args, available_flags):
args.append("--datarootdir=${prefix}/%s" % DEFAULT_SHARE)

with environment_append(pkg_env):
with environment_append(vars or self.vars):
command = '%s/configure %s %s' % (configure_dir, args_to_string(args),
" ".join(triplet_args))
self._conanfile.output.info("Calling:\n > %s" % command)
self._conanfile.run(command, win_bash=self._win_bash, subsystem=self.subsystem)"""

cmd = "bash -c 'source autotoolsdeps.sh && source autotools.sh && %s/configure'" % configure_dir
self._conanfile.output.info("Calling:\n > %s" % cmd)
self._conanfile.run(cmd, win_bash=self._win_bash, subsystem=self.subsystem)

def _configure_help_output(self, configure_path):
from six import StringIO # Python 2 and 3 compatible
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the new helper, I think we can avoid python 2 and six

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, ongoing work.

mybuf = StringIO()
try:
self._conanfile.run("%s/configure --help" % configure_path, win_bash=self._win_bash,
output=mybuf)
except ConanException as e:
self._conanfile.output.warn("Error running `configure --help`: %s" % e)
return ""
return mybuf.getvalue()

def _adjust_path(self, path):
if self._win_bash:
path = unix_path(path, path_flavor=self.subsystem)
return '"%s"' % path if " " in path else path

@staticmethod
def _valid_configure_flag(varname, args, available_flags):
return not AutoToolsBuildEnvironment._is_flag_in_args(varname, args) and \
varname in available_flags

@staticmethod
def _is_flag_in_args(varname, args):
flag = "--%s=" % varname
return any([flag in arg for arg in args])

def make(self, target=None):
"""if not self._build_type:
raise ConanException("build_type setting should be defined.")
with environment_append(vars or self.vars):
str_args = args_to_string(args)
cpu_count_option = (("-j%s" % cpu_count(output=self._conanfile.output))
if ("-j" not in str_args and "nmake" not in make_program.lower())
else None)
self._conanfile.run("%s" % join_arguments([make_program, target, str_args,
cpu_count_option]),
win_bash=self._win_bash, subsystem=self.subsystem)"""

make_program = self._conanfile.conf["tools.gnu"].make_program
if make_program is None:
make_program = "mingw32-make" if platform.system() == "Windows" else "make"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also don't think it's always desired to enforce mingw32-make on Windows. it should go from the configuration.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is probably a sane default in windows, the same as make is in other platforms. But we will see as we keep adding support for other subsystems, I am fine with removing it.

if platform.system() == "Windows":
cmd = "autotoolsdeps.bat && autotools.bat && {}".format(make_program)
else:
cmd = "bash -c 'source autotoolsdeps.sh "\
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we shouldn't hard-code bash here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless it is the only way to make it work reproducibly. The autotoolsdeps.sh, etc are using bash. If we are supporting other shells, we need tests, I don't want to add things into the new codebase that cannot be tested.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the make command is not hard-coded and got from the config.
the same, more or less, should be done for the bash I believe.
also, maybe it's enough to just use plain sh or $SHELL?
I don't think we need advanced bash-specific features, and it would be nice to be portable and require only posix sh.
bash is not always available and not a default shell everywhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The make command is hardcoded, most of the times it doesn't come from config:

make_program = os.getenv("CONAN_MAKE_PROGRAM") or make_program or "make"

I guess you are aware that many recipes in ConanCenter are doing make_program=which("make") or which("mingw32-make") to be able to work on Windows. No config here, but recipes hardcoding it.

Same as bash, hardcoded here:

return which("bash")
.

The fact that they have configs, doesn't mean that they are not hardcoded in the codebase. Same I am doing here, we can discuss some defaults as we gain knowledge, but I am not introducing new hardcoded things.

The new Environment files at the moment only work on bash. If we can generalize them to work on different shells, or to provide different generated files, for different shells, good for me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know there are recipes doing that, as I wrote some of them.
that might be a sign we're not managing things the best way.
in some cases, mingw32-make is valid defaults, in some cases valid default could be nmake or just plain make.
that might be the reason we need to provide helper to deduce sane default based on settings, e.g. it may return MinGW make, nmake, GNU make or BSD make depending on system and compiler.
but that we know already the logic doesn't believe to the AutoToolsBuildEnvironment or recipes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, agree, a helper would be the best. This helper can hardcode some defaults for some given configurations (settings, etc), and always have a [conf] to be able to override it.

But lets wait a bit and have a minimum set of build helpers, just the most common "AutotoolsXXX" ones, that work well and make recipes simpler for a majority of cases, before extracting new helpers like get_make_program(conanfile), when we gain more insights about these defaults, our new helpers will be closer to something stable.

"&& source autotools.sh && {}'".format(make_program)
self._conanfile.run(cmd, win_bash=self._win_bash, subsystem=self.subsystem)

def install(self, args=""):
if not self._conanfile.should_install:
return
self.make(target="install")

def _get_vars(self):
def append(*args):
ret = []
for arg in args:
if arg:
if isinstance(arg, list):
ret.extend(arg)
else:
ret.append(arg)
return ret

tmp_compilation_flags = copy.copy(self.flags)

if tools.is_apple_os(self._os):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it belongs here as well. let's keep all platform-specific code as minimal as possible, and isolated. the way we did it previously was proven to be fragile and doesn't scale well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, this code is not used at the moment, needs to be worked on.

concat = " ".join(tmp_compilation_flags)
if os.environ.get("CFLAGS", None):
concat += " " + os.environ.get("CFLAGS", None)
if os.environ.get("CXXFLAGS", None):
concat += " " + os.environ.get("CXXFLAGS", None)
if self._os_version and "-version-min" not in concat and "-target" not in concat:
tmp_compilation_flags.append(tools.apple_deployment_target_flag(self._os,
self._os_version,
self._os_sdk,
self._os_subsystem,
self._arch))
if "-isysroot" not in concat and platform.system() == "Darwin":
tmp_compilation_flags.extend(["-isysroot",
tools.XCRun(self._conanfile.settings).sdk_path])
if "-arch" not in concat and self._arch:
tmp_compilation_flags.extend(["-arch", tools.to_apple_arch(self._arch)])

cxx_flags = append(tmp_compilation_flags, self.cxx_flags, self.cppstd_flag)
c_flags = tmp_compilation_flags

return ld_flags, cpp_flags, libs, cxx_flags, c_flags