|
7 | 7 | import os |
8 | 8 | import re |
9 | 9 | import shlex |
| 10 | +from stat import ST_MODE |
10 | 11 |
|
11 | 12 | import SCons.Script |
12 | 13 | from SCons.Script.SConscript import SConsEnvironment |
13 | 14 |
|
14 | 15 | from . import state |
15 | 16 | from .installation import determineVersion, getFingerprint |
16 | | -from .utils import memberOf |
| 17 | +from .utils import memberOf, whichPython |
17 | 18 |
|
18 | 19 |
|
19 | 20 | @memberOf(SConsEnvironment) |
@@ -544,27 +545,32 @@ def Doxygen(self, config, **kwargs): |
544 | 545 | return builder(self, config) |
545 | 546 |
|
546 | 547 |
|
547 | | -@memberOf(SConsEnvironment) |
548 | | -def VersionModule(self, filename, versionString=None): |
| 548 | +def _get_version_string(versionString): |
549 | 549 | if versionString is None: |
550 | 550 | for n in ("git", "hg", "svn"): |
551 | 551 | if os.path.isdir(f".{n}"): |
552 | 552 | versionString = n |
553 | 553 |
|
554 | 554 | if not versionString: |
555 | 555 | versionString = "git" |
| 556 | + return versionString |
556 | 557 |
|
557 | | - def calcMd5(filename): |
558 | | - try: |
559 | | - import hashlib |
560 | 558 |
|
561 | | - md5 = hashlib.md5(open(filename, "rb").read()).hexdigest() |
562 | | - except OSError: |
563 | | - md5 = None |
| 559 | +def _calcMd5(filename): |
| 560 | + try: |
| 561 | + import hashlib |
| 562 | + |
| 563 | + md5 = hashlib.md5(open(filename, "rb").read()).hexdigest() |
| 564 | + except OSError: |
| 565 | + md5 = None |
564 | 566 |
|
565 | | - return md5 |
| 567 | + return md5 |
566 | 568 |
|
567 | | - oldMd5 = calcMd5(filename) |
| 569 | + |
| 570 | +@memberOf(SConsEnvironment) |
| 571 | +def VersionModule(self, filename, versionString=None): |
| 572 | + versionString = _get_version_string(versionString) |
| 573 | + oldMd5 = _calcMd5(filename) |
568 | 574 |
|
569 | 575 | def makeVersionModule(target, source, env): |
570 | 576 | try: |
@@ -629,10 +635,160 @@ def makeVersionModule(target, source, env): |
629 | 635 | outFile.write(f' "{n}",\n') |
630 | 636 | outFile.write(")\n") |
631 | 637 |
|
632 | | - if calcMd5(target[0].abspath) != oldMd5: # only print if something's changed |
| 638 | + if _calcMd5(target[0].abspath) != oldMd5: # only print if something's changed |
633 | 639 | state.log.info(f'makeVersionModule(["{target[0]}"], [])') |
634 | 640 |
|
635 | 641 | result = self.Command(filename, [], self.Action(makeVersionModule, strfunction=lambda *args: None)) |
636 | 642 |
|
637 | 643 | self.AlwaysBuild(result) |
638 | 644 | return result |
| 645 | + |
| 646 | + |
| 647 | +@memberOf(SConsEnvironment) |
| 648 | +def PackageInfo(self, pythonDir, versionString=None): |
| 649 | + versionString = _get_version_string(versionString) |
| 650 | + |
| 651 | + if not os.path.exists(pythonDir): |
| 652 | + return [] |
| 653 | + |
| 654 | + # Some information can come from the pyproject file. |
| 655 | + toml_metadata = {} |
| 656 | + if os.path.exists("pyproject.toml"): |
| 657 | + import tomllib |
| 658 | + |
| 659 | + with open("pyproject.toml", "rb") as fd: |
| 660 | + toml_metadata = tomllib.load(fd) |
| 661 | + |
| 662 | + toml_project = toml_metadata.get("project", {}) |
| 663 | + pythonPackageName = "" |
| 664 | + if "name" in toml_project: |
| 665 | + pythonPackageName = toml_project["name"] |
| 666 | + else: |
| 667 | + if os.path.exists(os.path.join(pythonDir, "lsst")): |
| 668 | + pythonPackageName = "lsst_" + state.env["packageName"] |
| 669 | + else: |
| 670 | + pythonPackageName = state.env["packageName"] |
| 671 | + pythonPackageName = pythonPackageName.replace("_", "-") |
| 672 | + # The directory name is required to use "_" instead of "-" |
| 673 | + distDir = os.path.join(pythonDir, f"{pythonPackageName.replace('-', '_')}.dist-info") |
| 674 | + filename = os.path.join(distDir, "METADATA") |
| 675 | + oldMd5 = _calcMd5(filename) |
| 676 | + |
| 677 | + def makePackageMetadata(target, source, env): |
| 678 | + # Create the metadata file. |
| 679 | + try: |
| 680 | + version = determineVersion(state.env, versionString) |
| 681 | + except RuntimeError: |
| 682 | + version = "unknown" |
| 683 | + |
| 684 | + os.makedirs(os.path.dirname(target[0].abspath), exist_ok=True) |
| 685 | + with open(target[0].abspath, "w") as outFile: |
| 686 | + print("Metadata-Version: 1.0", file=outFile) |
| 687 | + print(f"Name: {pythonPackageName}", file=outFile) |
| 688 | + print(f"Version: {version}", file=outFile) |
| 689 | + |
| 690 | + if _calcMd5(target[0].abspath) != oldMd5: # only print if something's changed |
| 691 | + state.log.info(f'PackageInfo(["{target[0]}"], [])') |
| 692 | + |
| 693 | + results = [] |
| 694 | + results.append( |
| 695 | + self.Command(filename, [], self.Action(makePackageMetadata, strfunction=lambda *args: None)) |
| 696 | + ) |
| 697 | + |
| 698 | + # Create the entry points file if defined in the pyproject.toml file. |
| 699 | + entryPoints = toml_project.get("entry-points", {}) |
| 700 | + if entryPoints: |
| 701 | + filename = os.path.join(distDir, "entry_points.txt") |
| 702 | + oldMd5 = _calcMd5(filename) |
| 703 | + |
| 704 | + def makeEntryPoints(target, source, env): |
| 705 | + # Make the entry points file as necessary. |
| 706 | + if not entryPoints: |
| 707 | + return |
| 708 | + os.makedirs(os.path.dirname(target[0].abspath), exist_ok=True) |
| 709 | + |
| 710 | + # Structure of entry points dict is something like: |
| 711 | + # "entry-points": { |
| 712 | + # "butler.cli": { |
| 713 | + # "pipe_base": "lsst.pipe.base.cli:get_cli_subcommands" |
| 714 | + # } |
| 715 | + # } |
| 716 | + # Which becomes a file with: |
| 717 | + # [butler.cli] |
| 718 | + # pipe_base = lsst.pipe.base.cli:get_cli_subcommands |
| 719 | + with open(target[0].abspath, "w") as fd: |
| 720 | + for entryGroup in entryPoints: |
| 721 | + print(f"[{entryGroup}]", file=fd) |
| 722 | + for entryPoint, entryValue in entryPoints[entryGroup].items(): |
| 723 | + print(f"{entryPoint} = {entryValue}", file=fd) |
| 724 | + |
| 725 | + if _calcMd5(target[0].abspath) != oldMd5: # only print if something's changed |
| 726 | + state.log.info(f'PackageInfo(["{target[0]}"], [])') |
| 727 | + |
| 728 | + if entryPoints: |
| 729 | + results.append( |
| 730 | + self.Command(filename, [], self.Action(makeEntryPoints, strfunction=lambda *args: None)) |
| 731 | + ) |
| 732 | + |
| 733 | + self.AlwaysBuild(results) |
| 734 | + return results |
| 735 | + |
| 736 | + |
| 737 | +@memberOf(SConsEnvironment) |
| 738 | +def PythonScripts(self): |
| 739 | + # Scripts are defined in the pyproject.toml file. |
| 740 | + toml_metadata = {} |
| 741 | + if os.path.exists("pyproject.toml"): |
| 742 | + import tomllib |
| 743 | + |
| 744 | + with open("pyproject.toml", "rb") as fd: |
| 745 | + toml_metadata = tomllib.load(fd) |
| 746 | + |
| 747 | + if not toml_metadata: |
| 748 | + return [] |
| 749 | + |
| 750 | + scripts = {} |
| 751 | + if "project" in toml_metadata and "scripts" in toml_metadata["project"]: |
| 752 | + scripts = toml_metadata["project"]["scripts"] |
| 753 | + |
| 754 | + def makePythonScript(target, source, env): |
| 755 | + cmdfile = target[0].abspath |
| 756 | + command = os.path.basename(cmdfile) |
| 757 | + if command not in scripts: |
| 758 | + return |
| 759 | + os.makedirs(os.path.dirname(cmdfile), exist_ok=True) |
| 760 | + package, func = scripts[command].split(":", maxsplit=1) |
| 761 | + with open(cmdfile, "w") as fd: |
| 762 | + # Follow setuptools convention and always change the shebang. |
| 763 | + # Can not add noqa on Linux for long paths so do not add anywhere. |
| 764 | + print( |
| 765 | + rf"""#!{whichPython()} |
| 766 | +import sys |
| 767 | +from {package} import {func} |
| 768 | +if __name__ == '__main__': |
| 769 | + sys.exit({func}()) |
| 770 | +""", |
| 771 | + file=fd, |
| 772 | + ) |
| 773 | + |
| 774 | + # Ensure the bin/ file is executable |
| 775 | + oldmode = os.stat(cmdfile)[ST_MODE] & 0o7777 |
| 776 | + newmode = (oldmode | 0o555) & 0o7777 |
| 777 | + if newmode != oldmode: |
| 778 | + state.log.info(f"Changing mode of {cmdfile} from {oldmode} to {newmode}") |
| 779 | + os.chmod(cmdfile, newmode) |
| 780 | + |
| 781 | + results = [] |
| 782 | + for cmd, code in scripts.items(): |
| 783 | + filename = f"bin/{cmd}" |
| 784 | + |
| 785 | + # Do not do anything if there is an equivalent target in bin.src |
| 786 | + # that shebang would trigger. |
| 787 | + if os.path.exists(f"bin.src/{cmd}"): |
| 788 | + continue |
| 789 | + |
| 790 | + results.append( |
| 791 | + self.Command(filename, [], self.Action(makePythonScript, strfunction=lambda *args: None)) |
| 792 | + ) |
| 793 | + |
| 794 | + return results |
0 commit comments