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

enhance CMakeMake easyblock to check whether correct Python installation was picked up by cmake #3233

Closed
wants to merge 15 commits into from

Conversation

MartinsNadia
Copy link

Sanity check to verify if Cmake apply the new policy CMP0094

ebrootpython_path = os.getenv('EBROOTPYTHON')
if all(path and ebrootpython_path in path for path in [pythonExecPath, pythonIncludePath, pythonLibraryPath]):
self.log.info("Python related paths configured correctly.")
return out
Copy link
Member

Choose a reason for hiding this comment

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

Move the return statement out of the if/else block, it is hidden here.

self.log.info("Python include path: %s", pythonIncludePath)
self.log.info("Python library path: %s", pythonLibraryPath)
# Check if paths include EBROOTPYTHON
ebrootpython_path = os.getenv('EBROOTPYTHON')
Copy link
Member

Choose a reason for hiding this comment

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

This assumes that EBROOTPYTHON is set, what happens if it isn't?

Copy link
Member

Choose a reason for hiding this comment

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

I imagine that what you want to do is complain if pythonExecPath (and friends) are set but no Python dependency is found

Comment on lines 334 to 336
self.log.info("Python executable path: %s", pythonExecPath)
self.log.info("Python include path: %s", pythonIncludePath)
self.log.info("Python library path: %s", pythonLibraryPath)
Copy link
Member

@ocaisa ocaisa Feb 27, 2024

Choose a reason for hiding this comment

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

I would only print these if they have a value (so move them up into your if/elif loop)

Copy link
Contributor

Choose a reason for hiding this comment

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

Not only that: You could have them multiple times when you consider Python_EXECUTABLE and Python3_EXECUTABLE or the variables may be completely unset if the CMake file didn't use Python at all

Maybe create thos variables as empty lists?


# Search for Python paths in each line
for line in lines:
if line.startswith('Python3_EXECUTABLE:FILEPATH='):
Copy link
Member

Choose a reason for hiding this comment

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

It looks like these are only guaranteed for CMake 3.16+ so should probably make this check conditional on that version

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes maybe use a regex? Python[2-3]?_EXECUTABLE?

self.log.info("Python include path: %s", pythonIncludePath)
self.log.info("Python library path: %s", pythonLibraryPath)
# Check if paths include EBROOTPYTHON
ebrootpython_path = os.getenv('EBROOTPYTHON')
Copy link
Member

@ocaisa ocaisa Feb 27, 2024

Choose a reason for hiding this comment

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

Suggested change
ebrootpython_path = os.getenv('EBROOTPYTHON')
ebrootpython_path = get_software_root("Python")
if pythonExecPath and not ebrootpython_path:
raise EasyBuildError("Python related path (%s) found but no Python dependency included, check log" % pythonExecPath)

# Search for Python paths in each line
for line in lines:
if line.startswith('Python3_EXECUTABLE:FILEPATH='):
pythonExecPath = line.split('=')[1].strip()
Copy link
Member

Choose a reason for hiding this comment

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

Small nitpicking suggestion: let's not start using CamelCase for variable names (we only use that for class names), so please use python_exec_path, python_include_path, python_library_path

@boegel boegel changed the title Sanity Check for Python Configuration enhance CMakeMake easyblock to check whether correct Python installation was picked up by cmake Feb 28, 2024
Copy link
Contributor

@Flamefire Flamefire left a comment

Choose a reason for hiding this comment

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

This might be a bit more tricky than it seems...


# sanitycheck for the configuration step
self.log.info("Checking python paths")
with open('CMakeCache.txt', 'r') as file:
Copy link
Contributor

Choose a reason for hiding this comment

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

This should handle the case that this file may not exist. E.g. when configure_cmd != DEFAULT_CONFIGURE_CMD anything can happen.

# sanitycheck for the configuration step
self.log.info("Checking python paths")
with open('CMakeCache.txt', 'r') as file:
lines = file.readlines()
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe use readfile(...).split('\n')?


# Search for Python paths in each line
for line in lines:
if line.startswith('Python3_EXECUTABLE:FILEPATH='):
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes maybe use a regex? Python[2-3]?_EXECUTABLE?

Comment on lines 334 to 336
self.log.info("Python executable path: %s", pythonExecPath)
self.log.info("Python include path: %s", pythonIncludePath)
self.log.info("Python library path: %s", pythonLibraryPath)
Copy link
Contributor

Choose a reason for hiding this comment

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

Not only that: You could have them multiple times when you consider Python_EXECUTABLE and Python3_EXECUTABLE or the variables may be completely unset if the CMake file didn't use Python at all

Maybe create thos variables as empty lists?

self.log.info("Python library path: %s", pythonLibraryPath)
# Check if paths include EBROOTPYTHON
ebrootpython_path = os.getenv('EBROOTPYTHON')
if all(path and ebrootpython_path in path for path in [pythonExecPath, pythonIncludePath, pythonLibraryPath]):
Copy link
Contributor

Choose a reason for hiding this comment

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

See above: pythonExecPath & co might not have been set at all. And maybe path.startswith(ebrootpython_path)? We possibly need to take symlinks into account too: either of the 2 might be a resolved symlink or not.

except FileNotFoundError:
self.log.warning("CMakeCache.txt not founf. Python paths checks skipped.")
return
# EBROOTPYTHON check
Copy link
Contributor

Choose a reason for hiding this comment

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

And please remove this comment. It doesn't add any information to the code below

Copy link
Contributor

@Flamefire Flamefire left a comment

Choose a reason for hiding this comment

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

Please recheck also the use(s) of self.log.error. When raising an EasyBuildError you don't need to log the same because that will already be done.
I also thing that you don't need self.log.error but only EasyBuildError, i.e. fail if there is an error.

Another enhancement would be to gather all issues (e.g. each wrong path) in a list and then fail if that list is non-empty. This way ALL issues would be reported, not only the first found which might help in fixing this.

python_prefixes = r"(Python|Python2|Python3)_"

for line in lines:
if line.startswith(python_prefixes + r"EXECUTABLE:FILEPATH="):
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't work. python_prefixes is all lowercase and you likely intended to use something like re.match. Got to look up the docs but from the top of my head: python_prefixes = "PYTHON[23]?_" above and re.match(python_prefixes + "EXECUTABLE(:FILEPATH)?=", line) here would be needed

python_library_path.append(line.split('=')[1].strip())
self.log.info("Python library path: %s", python_library_path[-1])

# Check if paths are found and validate paths for symlinks
Copy link
Contributor

Choose a reason for hiding this comment

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

Everything from here should be outside the loop shouldn't it?


# Check if paths are found and handle EBROOTPYTHON cases
if any(path for path in [python_exec_path, python_include_path, python_library_path]):
if not os.getenv('EBROOTPYTHON'):
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like a duplication to the below (ebrootpython_path etc). Maybe at least put them together? Are both even required? I.e. isn't ebrootpython_path enough/better than EBROOTPYTHON?

)
elif not all(
(path and os.getenv('EBROOTPYTHON') in path)
for path in [python_exec_path, python_include_path, python_library_path]
Copy link
Contributor

Choose a reason for hiding this comment

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

You are checking only for $EBROOTPYTHON being in a list of paths, which likely is always false. Maybe rename the variables to be able to spot that: python_exec_path -> python_exec_paths and so on.

It might even make sense to show which path is "wrong". I.e. which of the 3 lists and which exact path is outside EB.

MartinsNadia and others added 3 commits April 30, 2024 10:27
- Reintroduce function such that missing early returns can be used
- Fix regexp (Superflous group, optional stuff)
- Handle the case where the cache path is not found
- Handle $EBROOTPYTHON not set and comparing it in the presence of symlinks
Improve Python check in cmakemake.py
@boegel boegel added this to the 4.x milestone Jun 25, 2024

return out

def check_python_paths(self):
"""Check that there are no detected Python paths outside the EB installed PythonF"""
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"""Check that there are no detected Python paths outside the EB installed PythonF"""
"""Check that there are no detected Python paths outside the Python dependency provided by EasyBuild"""

self.log.warning("CMakeCache.txt not found. Python paths checks skipped.")
return
cmake_cache = read_file('CMakeCache.txt')
if not cmake_cache:
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I don't see how this can happen, since at this point you know that the file exists?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, but it could be empty for some reason

"include_dir": [],
"library": [],
}
PYTHON_RE = re.compile(r"_?(?:Python|PYTHON)\d_(EXECUTABLE|INCLUDE_DIR|LIBRARY)[^=]*=(.*)")
Copy link
Member

Choose a reason for hiding this comment

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

Don't use "constants" inline:

Suggested change
PYTHON_RE = re.compile(r"_?(?:Python|PYTHON)\d_(EXECUTABLE|INCLUDE_DIR|LIBRARY)[^=]*=(.*)")
python_regex = re.compile(r"_?(?:Python|PYTHON)\d_(EXECUTABLE|INCLUDE_DIR|LIBRARY)[^=]*=(.*)")

}
PYTHON_RE = re.compile(r"_?(?:Python|PYTHON)\d_(EXECUTABLE|INCLUDE_DIR|LIBRARY)[^=]*=(.*)")
for line in cmake_cache.splitlines():
match = PYTHON_RE.match(line)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
match = PYTHON_RE.match(line)
match = python_regex.match(line)

ebrootpython_path = get_software_root("Python")
if not ebrootpython_path:
if any(python_paths.values()) and not self.toolchain.comp_family() == toolchain.SYSTEM:
self.log.warning("Found Python paths in CMake cache but Python is not a dependency")
Copy link
Member

Choose a reason for hiding this comment

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

I wouldn't make this a warning, just info message?

Suggested change
self.log.warning("Found Python paths in CMake cache but Python is not a dependency")
self.log.info("Found Python paths in CMake cache but Python is not a dependency")

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 not, it is likely an error. #3233 (comment) requested to "complain" ;-)

if path.endswith('-NOTFOUND'):
continue
if not os.path.exists(path):
errors.append(f"Python {path_type} does not exist: {path}")
Copy link
Member

Choose a reason for hiding this comment

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

Since this is a PR to develop, we need to be careful with f-strings, since EasyBuild 4.x is still supposed to support Python 2.7:

Suggested change
errors.append(f"Python {path_type} does not exist: {path}")
errors.append("Python %s does not exist: %s" % (path_type, path))

if match:
path_type = match[1].lower()
path = match[2].strip()
self.log.info(f"Python {path_type} path: {path}")
Copy link
Member

Choose a reason for hiding this comment

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

Since this is a PR to develop, we need to be careful with f-strings, since EasyBuild 4.x is still supposed to support Python 2.7:

Suggested change
self.log.info(f"Python {path_type} path: {path}")
self.log.info("Python %s path: %s" % (path_type, path))

Copy link
Contributor

Choose a reason for hiding this comment

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

No need to format before logging:

Suggested change
self.log.info(f"Python {path_type} path: {path}")
self.log.info("Python %s path: %s", path_type, path)

if not os.path.exists(path):
errors.append(f"Python {path_type} does not exist: {path}")
elif not os.path.realpath(path).startswith(ebrootpython_path):
errors.append(f"Python {path_type} path '{path}' is outside EBROOTPYTHON ({ebrootpython_path})")
Copy link
Member

Choose a reason for hiding this comment

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

Since this is a PR to develop, we need to be careful with f-strings, since EasyBuild 4.x is still supposed to support Python 2.7:

Suggested change
errors.append(f"Python {path_type} path '{path}' is outside EBROOTPYTHON ({ebrootpython_path})")
errors.append("Python %s path '%s' is outside EBROOTPYTHON (%s)" % (path_type, path, ebrootpython_path))

if errors:
# Combine all errors into a single message
error_message = "\n".join(errors)
raise EasyBuildError(f"Python path errors:\n{error_message}")
Copy link
Member

Choose a reason for hiding this comment

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

Since this is a PR to develop, we need to be careful with f-strings, since EasyBuild 4.x is still supposed to support Python 2.7:

Suggested change
raise EasyBuildError(f"Python path errors:\n{error_message}")
raise EasyBuildError("Python path errors:\n" + error_message)

@boegel
Copy link
Member

boegel commented Jun 25, 2024

@boegelbot please test @ generoso
EB_ARGS="--installpath /tmp/pr3233 FlexiBLAS-3.3.1-GCC-12.3.0.eb SPAdes-3.15.4-GCC-12.3.0.eb Doxygen-1.9.7-GCCcore-12.3.0.eb poppler-23.09.0-GCC-12.3.0.eb"

@boegelbot
Copy link

@boegel: Request for testing this PR well received on login1

PR test command 'EB_PR=3233 EB_ARGS="--installpath /tmp/pr3233 FlexiBLAS-3.3.1-GCC-12.3.0.eb SPAdes-3.15.4-GCC-12.3.0.eb Doxygen-1.9.7-GCCcore-12.3.0.eb poppler-23.09.0-GCC-12.3.0.eb" EB_CONTAINER= EB_REPO=easybuild-easyblocks /opt/software/slurm/bin/sbatch --job-name test_PR_3233 --ntasks=4 ~/boegelbot/eb_from_pr_upload_generoso.sh' executed!

  • exit code: 0
  • output:
Submitted batch job 13805

Test results coming soon (I hope)...

- notification for comment with ID 2188166362 processed

Message to humans: this is just bookkeeping information for me,
it is of no use to you (unless you think I have a bug, which I don't).

@boegelbot
Copy link

Test report by @boegelbot

Overview of tested easyconfigs (in order)

  • SUCCESS FlexiBLAS-3.3.1-GCC-12.3.0.eb
  • SUCCESS SPAdes-3.15.4-GCC-12.3.0.eb
  • SUCCESS Doxygen-1.9.7-GCCcore-12.3.0.eb
  • SUCCESS poppler-23.09.0-GCC-12.3.0.eb

Build succeeded for 4 out of 4 (4 easyconfigs in total)
cns1 - Linux Rocky Linux 8.9, x86_64, Intel(R) Xeon(R) CPU E5-2667 v3 @ 3.20GHz (haswell), Python 3.6.8
See https://gist.github.com/boegelbot/65f7160953f1af9e1a9f587844f085be for a full test report.

@Flamefire
Copy link
Contributor

I added MartinsNadia#3 to get this done fast. It addresses the review and handles the "FindPython" (w/o version number) case.

@boegel
Copy link
Member

boegel commented Jul 3, 2024

@MartinsNadia ping on reviewing/merging MartinsNadia#3 to update this PR?

@Flamefire
Copy link
Contributor

As @bedroge retargeted this to 5.x which creates difficult conflicts due to the considerable changes during review I created #3399 combining those 15 commits into 2

@boegel boegel modified the milestones: 4.9.3, release after 4.9.3 Sep 11, 2024
@boegel
Copy link
Member

boegel commented Oct 16, 2024

These changes (and some enhancements on top) got merged via #3399, so closing this (it's not being auto-closed because commits were squashed in #3399)

@boegel boegel closed this Oct 16, 2024
@boegel boegel modified the milestones: release after 4.9.4, 5.0 Oct 16, 2024
@boegel boegel added the EasyBuild-5.0 EasyBuild 5.0 label Oct 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants