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

add generic SystemMPI easyblock to generate module for existing MPI library installation #1106

Merged
merged 64 commits into from
Oct 11, 2017
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
c2fcd86
First go at a system OpenMPI easyblock
Feb 10, 2017
b3bdaae
Switch computer
Feb 13, 2017
902b9cc
Make extract_compiler_version available to SystemMPI
Feb 14, 2017
8f789ce
Update outdated comments
Feb 15, 2017
da237ca
Fix variable typo
Feb 15, 2017
d3d4c5f
Correctly update version in make_module_extend_modpath
Feb 15, 2017
02520ce
Correct syntax problem
Feb 15, 2017
3309b16
Add support for impi
Feb 16, 2017
6d380fe
Address review comments
Feb 16, 2017
f89a9e4
Add OpenMPI to travis
Feb 16, 2017
78f6115
Allow tests on Travis to pass
Feb 16, 2017
5e7eaa5
Allow tests on Travis to pass
Feb 16, 2017
7fc6265
Typo
Feb 16, 2017
b9ced9d
Add another cheat to get Travis to pass his tests
Feb 16, 2017
1877b18
Stop cheating on tests
Feb 16, 2017
c52c92d
Add special case to tests for SystemMPI
Feb 16, 2017
44026e6
Roll back toolchain addition in tests
Feb 16, 2017
7b7f704
Add special case for dummy to module-only test
Feb 16, 2017
abacb35
Fix bugs in impi path
Feb 16, 2017
cfa57f6
Need to go one deeper in search for impi root due to symlink
Feb 16, 2017
212e53a
systemcompiler is not good enough to resolve compilers not in the def…
Feb 16, 2017
bcab0fa
Update systemcompiler.py
Feb 16, 2017
c51a0f7
Add option to systemcompiler to attempt to find the path extensions f…
Feb 17, 2017
4f76ca0
Merge remote-tracking branch 'origin/systemmpi' into systemmpi
Feb 17, 2017
7850024
Add author
Feb 17, 2017
eae2b8e
Implement path extensions for systemmpi
Feb 17, 2017
f4a3ea6
Implement path extensions for systemmpi
Feb 17, 2017
5a88443
Make sure systemcompiler finds the correct install paths for version …
Feb 17, 2017
b0ee25e
Try to get system compiler to match the correct version of intel comp…
Feb 17, 2017
f144400
Fix compiler version detection for intel
Feb 17, 2017
7b525b0
Fix dodgy variable name
Feb 17, 2017
60b86ce
Allow picking up of the licence for intel compilers
Feb 17, 2017
3d5263c
Make sure 201* is treated as a number
Feb 17, 2017
4096508
Support older version of impi
Feb 23, 2017
8e32528
Address comments
Mar 1, 2017
ccee79b
Use the GCC init as the bundle causes errors because of unexpected va…
Mar 1, 2017
89e13be
Change order in extra_options to avoid the error in __init__
Mar 1, 2017
637a815
Change order in extra_options to avoid the error in __init__
Mar 1, 2017
5f17da8
Add arguments to Bundle.make_module_extra
Mar 1, 2017
b62ac7c
Fix error with make_module_extra when not having __init__ from the bu…
Mar 1, 2017
8d0dd01
Ignore the intelbase prepare_step, just use the bundle one
Mar 1, 2017
e3985f9
Try to overcome make_module_extra error
Mar 1, 2017
53b3c34
Try to overcome make_module_extra error by passing the arguments down
Mar 1, 2017
e88e2a1
Try to overcome make_module_extra error by passing the arguments down
Mar 1, 2017
0078d2f
Try to overcome make_module_extra error by passing the arguments down
Mar 1, 2017
0250ce3
Try to overcome make_module_extra error by passing the arguments down
Mar 1, 2017
6eaf360
Add missing LooseVersion
Mar 1, 2017
2f5a09d
Add missing LooseVersion
Mar 1, 2017
4a3cc32
Address comments for systemcompiler.py
Sep 15, 2017
61ba4e5
Merge branch 'develop' into systemmpi
Sep 15, 2017
58cb72a
Address comments for systemmpi.py
Sep 15, 2017
fddb19b
Allow either icc or gcc to sit underneath impi
Sep 15, 2017
a1facb1
Add `*args, **kwargs` to prepare_step(s)
Sep 15, 2017
570cf47
Add `*args, **kwargs` to prepare_step(s)...in full this time
Sep 15, 2017
3a1496a
Add `*args, **kwargs` to prepare_step(s)...in full this time
Sep 15, 2017
951a974
Revert silly changes
Sep 15, 2017
7594102
Fix attribute bug
Sep 15, 2017
477002f
pass in post_install_step to avoid problems with symlinks created by …
boegel Sep 30, 2017
c90a799
Merge pull request #10 from boegel/systemmpi
Sep 30, 2017
f35d52a
fix error reporting for missing MPI compiler wrapper for impi
boegel Sep 30, 2017
2206203
Merge pull request #11 from boegel/systemmpi
Sep 30, 2017
71cec42
pass down *args and **kwargs in make_module_extra rather than working…
boegel Oct 11, 2017
aa2e77e
improve error reporting for missing compiler, fix error reporting for…
boegel Oct 11, 2017
a43637a
Merge pull request #12 from boegel/systemmpi
Oct 11, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ addons:
apt:
packages:
- tcl8.5
before_install:
- sudo apt-get update && sudo apt-get install -y python-dev torque-server libtorque2-dev libopenmpi-dev openmpi-bin
Copy link
Member

Choose a reason for hiding this comment

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

@ocaisa Maybe include a comment to explain why this is needed?

Also, why do you need Torque at all?

install:
# install easybuild-framework (and dependencies)
# prefer clone & easy_install over easy_install directly in order to obtain information of what was installed exactly
Expand Down
53 changes: 32 additions & 21 deletions easybuild/easyblocks/generic/systemcompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,39 @@
"""
import os
import re
from vsc.utils import fancylogger

from easybuild.easyblocks.generic.bundle import Bundle
from easybuild.tools.filetools import read_file, which
from easybuild.tools.run import run_cmd
from easybuild.framework.easyconfig.easyconfig import ActiveMNS
from easybuild.tools.build_log import EasyBuildError

_log = fancylogger.getLogger('easyblocks.generic.systemcompiler')

def extract_compiler_version(compiler_name):
"""Extract compiler version for provided compiler_name."""
# look for 3-4 digit version number, surrounded by spaces
# examples:
# gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-11)
# Intel(R) C Intel(R) 64 Compiler XE for applications running on Intel(R) 64, Version 15.0.1.133 Build 20141023
if compiler_name == 'gcc':
out, _ = run_cmd("gcc --version", simple=False)
elif compiler_name in ['icc', 'ifort']:
out, _ = run_cmd("%s -V" % compiler_name, simple=False)
Copy link
Member Author

@ocaisa ocaisa Feb 17, 2017

Choose a reason for hiding this comment

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

@geimer The version extracted here doesn't actually match the version used by EasyBuild:

extracted:  17.0.0.098
EB version: 2017.0.098

I could just extract that from the product path (for <2016 one search string, for later stuff another)

else:
raise EasyBuildError("Unknown compiler %s" % compiler_name)

version_regex = re.compile(r'\s([0-9]+(?:\.[0-9]+){1,3})\s', re.M)
res = version_regex.search(out)
if res:
compiler_version = res.group(1)
_log.debug("Extracted compiler version '%s' for %s from: %s", compiler_version, compiler_name, out)
else:
raise EasyBuildError("Failed to extract compiler version for %s using regex pattern '%s' from: %s",
compiler_name, version_regex.pattern, txt)

return compiler_version

class SystemCompiler(Bundle):
"""
Expand All @@ -48,46 +74,31 @@ class SystemCompiler(Bundle):
if an actual version is specified, it is checked against the derived version of the system compiler that was found.
"""

def extract_compiler_version(self, txt):
"""Extract compiler version from provided string."""
# look for 3-4 digit version number, surrounded by spaces
# examples:
# gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-11)
# Intel(R) C Intel(R) 64 Compiler XE for applications running on Intel(R) 64, Version 15.0.1.133 Build 20141023
version_regex = re.compile(r'\s([0-9]+(?:\.[0-9]+){1,3})\s', re.M)
res = version_regex.search(txt)
if res:
self.compiler_version = res.group(1)
self.log.debug("Extracted compiler version '%s' from: %s", self.compiler_version, txt)
else:
raise EasyBuildError("Failed to extract compiler version using regex pattern '%s' from: %s",
version_regex.pattern, txt)

def __init__(self, *args, **kwargs):
"""Extra initialization: determine system compiler version and prefix."""
super(SystemCompiler, self).__init__(*args, **kwargs)

# Determine compiler path (real path, with resolved symlinks)
compiler_name = self.cfg['name'].lower()
if compiler_name == 'gcccore':
compiler_name = 'gcc'
path_to_compiler = which(compiler_name)
if path_to_compiler:
path_to_compiler = os.path.realpath(path_to_compiler)
Copy link
Member

Choose a reason for hiding this comment

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

here too, use resolve_path

self.log.info("Found path to compiler '%s' (with symlinks resolved): %s", compiler_name, path_to_compiler)
else:
raise EasyBuildError("%s not found in $PATH", compiler_name)

# Determine compiler version and installation prefix
if compiler_name == 'gcc':
out, _ = run_cmd("gcc --version", simple=False)
self.extract_compiler_version(out)
# Determine compiler version
self.compiler_version = extract_compiler_version(compiler_name)

# Determine installation prefix
if compiler_name == 'gcc':
# strip off 'bin/gcc'
self.compiler_prefix = os.path.dirname(os.path.dirname(path_to_compiler))

elif compiler_name in ['icc', 'ifort']:
out, _ = run_cmd("%s -V" % compiler_name, simple=False)
self.extract_compiler_version(out)

intelvars_fn = path_to_compiler + 'vars.sh'
if os.path.isfile(intelvars_fn):
self.log.debug("Trying to determine compiler install prefix from %s", intelvars_fn)
Expand Down
249 changes: 249 additions & 0 deletions easybuild/easyblocks/generic/systemmpi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
##
# Copyright 2015-2017 Ghent University
#
# This file is part of EasyBuild,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
# with support of Ghent University (http://ugent.be/hpc),
# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be),
# Flemish Research Foundation (FWO) (http://www.fwo.be/en)
# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
#
# http://github.com/hpcugent/easybuild
#
# EasyBuild 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 v2.
#
# EasyBuild 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 EasyBuild. If not, see <http://www.gnu.org/licenses/>.
##
"""
EasyBuild support for using (already installed/existing) system MPI instead of a full install via EasyBuild.

@author Alan O'Cais (Juelich Supercomputing Centre)
"""
import os
import re
from vsc.utils import fancylogger

from easybuild.easyblocks.generic.bundle import Bundle
from easybuild.easyblocks.generic.systemcompiler import extract_compiler_version
from easybuild.tools.modules import get_software_version
from easybuild.tools.filetools import read_file, which
Copy link
Member

Choose a reason for hiding this comment

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

read_file is not used

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 now :)

from easybuild.tools.run import run_cmd
from easybuild.framework.easyconfig.easyconfig import ActiveMNS
from easybuild.tools.build_log import EasyBuildError

_log = fancylogger.getLogger('easyblocks.generic.systemmpi')
Copy link
Member

Choose a reason for hiding this comment

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

not used, so remove (incl. import for fancylogger)


class SystemMPI(Bundle):
"""
Support for generating a module file for the system mpi with specified name.

The mpi compiler is expected to be available in $PATH, required libraries are assumed to be readily available.

Specifying 'system' as a version leads to using the derived mpi version in the generated module;
if an actual version is specified, it is checked against the derived version of the system mpi that was found.
"""

def extract_ompi_setting(self, pattern, txt):
"""Extract a particular OpenMPI setting from provided string."""

version_regex = re.compile(r'^\s+%s: (.*)$' % pattern, re.M)
res = version_regex.search(txt)
if res:
setting = res.group(1)
self.log.debug("Extracted OpenMPI setting %s: '%s' from search text", pattern, setting)
else:
raise EasyBuildError("Failed to extract OpenMPI setting '%s' using regex pattern '%s' from: %s",
pattern, version_regex.pattern, txt)

return setting

def __init__(self, *args, **kwargs):
"""Extra initialization: determine system MPI version, prefix and any associated envvars."""
super(SystemMPI, self).__init__(*args, **kwargs)

# If we are testing on Travis let's cheat (since it has been set up to have OpenMPI installed)
if self.cfg['name'] == 'foo':
Copy link
Member

Choose a reason for hiding this comment

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

hmm, no :)

we should find another way for this...

self.cfg['name'] = 'OpenMPI'
self.cfg['version'] = 'system'

mpi_name = self.cfg['name'].lower()

# Determine MPI wrapper path (real path, with resolved symlinks) to ensure it exists
if mpi_name == 'impi':
mpi_c_wrapper = 'mpiicc'
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, this is only right if you're using Intel MPI on top of Intel compilers, you don't know if that's the case 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.

Oh, that is a bit of a tough one because the line in that script I'm using to figure out the version doesn't exist in the impi mpicc

Copy link
Member Author

Choose a reason for hiding this comment

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

That's weird, it does exist for mpif90 and mpigcc, I guess I can use mpigcc...but isn't that making another assumption that that exists?

In general, are you saying that if you didn't have intel compilers available that the mpiicc script would not exist? As it stands I only use it to extract the version info for impi so I am indifferent to which command I use...I just want to be certain the script exists

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess I can it make it barf if mpiicc or mpigcc do not exist.

Copy link
Member

Choose a reason for hiding this comment

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

If you're only using it to determine the version, it's fine.

Maybe trying to use mpiicc will just fail if the Intel compilers are not available, I'm not sure.

We can deal with that if the problem occurs though...

else:
mpi_c_wrapper = 'mpicc'
path_to_mpi_c_wrapper = which(mpi_c_wrapper)
if path_to_mpi_c_wrapper:
path_to_mpi_c_wrapper = os.path.realpath(path_to_mpi_c_wrapper)
Copy link
Member

Choose a reason for hiding this comment

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

we now have resolve_path for this (from filetools)

path_to_mpi_c_wrapper = resolve_path(path_to_mpi_c_wrapper)

self.log.info("Found path to MPI implementation '%s' %s compiler (with symlinks resolved): %s",
mpi_name, mpi_c_wrapper, path_to_mpi_c_wrapper)
else:
raise EasyBuildError("%s not found in $PATH", mpi_c_wrapper)

# Determine compiler version and installation prefix
if mpi_name == 'openmpi':
output_of_ompi_info, _ = run_cmd("ompi_info", simple=False)
# Extract the version of OpenMPI
self.mpi_version = self.extract_ompi_setting("Open MPI", output_of_ompi_info)

# Extract the installation prefix
self.mpi_prefix = self.extract_ompi_setting("Prefix", output_of_ompi_info)

# Extract any OpenMPI environment variables in the current environment and ensure they are added to the
# final module
self.mpi_envvars = dict((key, value) for key, value in os.environ.iteritems() if key.startswith("OMPI_"))

# Extract the C compiler used underneath OpenMPI, check for the definition of OMPI_MPICC
self.mpi_c_compiler = self.extract_ompi_setting("C compiler", output_of_ompi_info)

elif mpi_name == 'impi':
# Extract the version of IntelMPI
# The prefix in the the mpiicc script can be used to extract the explicit version
contents_of_mpiicc, _ = read_file(path_to_mpi_c_wrapper)
prefix_regex = re.compile(r'(?<=compilers_and_libraries_)(.*)(?=/linux/mpi)', re.M)
self.mpi_version = prefix_regex.search(contents_of_mpiicc)
Copy link
Member

Choose a reason for hiding this comment

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

double-check to make sure the version was indeed found (i.e. not None)

Copy link
Member Author

Choose a reason for hiding this comment

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

I've implemented that further down for the generic case

if self.mpi_version is not None:
Copy link
Member

Choose a reason for hiding this comment

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

use positive logic if self.mpi_version is None...

self.log.info("Found Intel MPI version %s for system MPI" % self.mpi_version)
else:
raise EasyBuildError("No version found for system Intel MPI")

# Extract the installation prefix, if I_MPI_ROOT is defined, let's use that
if os.environ['I_MPI_ROOT']:
self.mpi_prefix = os.environ['I_MPI_ROOT']
else:
# Else just go up two directories from where mpiicc is found
self.mpi_prefix = os.path.dirname(os.path.dirname(mpi_c_wrapper))

if not os.path.exists(self.mpi_prefix):
raise EasyBuildError("Path derived for Intel MPI (%s) does not exist!", self.mpi_prefix)


# Extract any IntelMPI environment variables in the current environment and ensure they are added to the
# final module
self.mpi_envvars = dict((key, value) for key, value in os.environ.iteritems() if key.startswith("I_MPI_"))
self.mpi_envvars += dict((key, value) for key, value in os.environ.iteritems() if key.startswith("MPICH_"))
self.mpi_envvars += dict((key, value) for key, value in os.environ.iteritems()
if key.startswith("MPI") and key.endswith("PROFILE"))

# Extract the C compiler used underneath Intel MPI
compile_info, _ = run_cmd("%s -compile-info", simple=False)
self.mpi_c_compiler = compile_info.split(' ', 1)[0]

else:
raise EasyBuildError("Unrecognised system MPI implementation %s", mpi_name)

# Ensure install path of system MPI actually exists
if not os.path.exists(self.mpi_prefix):
raise EasyBuildError("Path derived for system MPI (%s) does not exist: %s!", mpi_name, self.mpi_prefix)

self.log.debug("Derived version/install prefix for system MPI %s: %s, %s",
mpi_name, self.mpi_version, self.mpi_prefix)

# For the version of the underlying C compiler need to explicitly extract (to be certain)
self.c_compiler_version = extract_compiler_version(self.mpi_c_compiler)
self.log.debug("Derived compiler/version for C compiler underneath system MPI %s: %s, %s",
mpi_name, self.mpi_c_compiler, self.c_compiler_version)

# If EasyConfig specified "real" version (not 'system' which means 'derive automatically'), check it
if self.cfg['version'] == 'system':
self.log.info("Found specified version '%s', going with derived MPI version '%s'",
self.cfg['version'], self.mpi_version)
elif self.cfg['version'] == self.mpi_version:
self.log.info("Specified MPI version %s matches found version" % self.mpi_version)
else:
raise EasyBuildError("Specified version (%s) does not match version reported by MPI (%s)" %
(self.cfg['version'], self.mpi_version))
Copy link
Member

Choose a reason for hiding this comment

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

use , rather than %, both in EasyBuildError and self.log.*


# fix installdir and module names (may differ because of changes to version)
mns = ActiveMNS()
self.cfg.full_mod_name = mns.det_full_module_name(self.cfg)
self.cfg.short_mod_name = mns.det_short_module_name(self.cfg)
self.cfg.mod_subdir = mns.det_module_subdir(self.cfg)

# keep track of original values, for restoring later
self.orig_version = self.cfg['version']
self.orig_installdir = self.installdir

def make_installdir(self, dontcreate=None):
"""Custom implementation of make installdir: do nothing, do not touch system MPI directories and files."""
pass

def make_module_req_guess(self):
"""
A dictionary of possible directories to look for. Return appropriate dict for system MPI.
"""
if self.cfg['name'] == "impi":
# Need some extra directories for Intel MPI, assuming 64bit here
lib_dirs = ['lib/em64t', 'lib64']
include_dirs = ['include64']
return_dict = {
'PATH': ['bin/intel64', 'bin64'],
'LD_LIBRARY_PATH': lib_dirs,
'LIBRARY_PATH': lib_dirs,
'CPATH': include_dirs,
'MIC_LD_LIBRARY_PATH': ['mic/lib'],
}
else:
return_dict = {}

return return_dict

def make_module_step(self, fake=False):
"""
Custom module step for SystemMPI: make 'EBROOT' and 'EBVERSION' reflect actual system MPI version
and install path.
"""
# First let's verify that the toolchain and the compilers under MPI match
c_compiler_name = self.toolchain.COMPILER_CC
Copy link
Member

Choose a reason for hiding this comment

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

move this into the else

compiler_version = get_software_version(self.toolchain.COMPILER_MODULE_NAME[0])

if self.mpi_c_compiler != c_compiler_name or self.c_compiler_version != compiler_version:
raise EasyBuildError("C compiler for toolchain (%s/%s) and underneath MPI (%s/%s) do not match!",
c_compiler_name, compiler_version, self.mpi_c_compiler, self.c_compiler_version)

# For module file generation: temporarily set version and installdir to system compiler values
Copy link
Member

Choose a reason for hiding this comment

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

s/compiler/MPI/g

self.cfg['version'] = self.mpi_version
self.installdir = self.mpi_prefix

# Generate module
res = super(SystemMPI, self).make_module_step(fake=fake)

# Reset version and installdir to EasyBuild values
self.installdir = self.orig_installdir
self.cfg['version'] = self.orig_version
return res

def make_module_extend_modpath(self):
"""
Custom prepend-path statements for extending $MODULEPATH: use version specified in easyconfig file (e.g.,
"system") rather than the actual version (e.g., "4.8.2").
Copy link
Member

Choose a reason for hiding this comment

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

s/4.8.2/2.0.2/g

"""
# temporarly set switch back to version specified in easyconfig file (e.g., "system")
self.cfg['version'] = self.orig_version

# Retrieve module path extensions
res = super(SystemMPI, self).make_module_extend_modpath()

# Reset to actual MPI version (e.g., "4.8.2")
Copy link
Member

Choose a reason for hiding this comment

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

use 2.0.2 since this refers to Open MPI version for example?

self.cfg['version'] = self.mpi_version
return res

def make_module_extra(self):
"""Add all the extra environment variables we found."""
txt = super(SystemMPI, self).make_module_extra()

# include environment variables defined for MPI implementation
for key, val in sorted(self.mpi_envvars.items()):
txt += self.module_generator.set_environment(key, val)

self.log.debug("make_module_extra added this: %s" % txt)
return txt