import datetime from pathlib import Path import sys from typing import Annotated, Optional import typer from subprocess import Popen, PIPE import subprocess import os import re import shutil sys.stdin.reconfigure(encoding='utf-8') sys.stdout.reconfigure(encoding='utf-8') __VERSION__ = "1.2" __AUTHORS__ = ("Dj_Haski",) __LICENSE__ = "MIT" app = typer.Typer() def print_header(): print(f'Fortrun by {", ".join(__AUTHORS__)} v{__VERSION__} ({__LICENSE__})') print( f'Copyright (c) 2022-{datetime.datetime.now().year}, {", ".join(__AUTHORS__)}\n' ) def errexit(message: str): typer.echo(typer.style(f"error: ", fg=typer.colors.RED) + message) typer.echo( "Compilation " + typer.style(f"failed", fg=typer.colors.RED) + ", exiting..." ) sys.exit(-1) def info(message: str): typer.echo(typer.style(f"info: ", fg=typer.colors.CYAN) + message) def hint(message: str): typer.echo(typer.style(f"hint: ", fg=typer.colors.YELLOW) + message) def safe_check_output(command, encoding='utf-8'): try: return subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, encoding=encoding) except subprocess.CalledProcessError as e: return e.output def try_or_exit(command: str): try: subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT) return True except subprocess.CalledProcessError as e: hint( "Looks like compilation or linking failed. Perhaps you forgot to add some libraries or some source files? Check your source code and try again." ) errexit(f"Failed to execute command '{command}'") def format_number(number: float): if number.is_integer(): return str(int(number)).ljust(12) else: return f"{number:.12f}".rstrip('0').ljust(12) def check_fl32_output(): output = safe_check_output('fl32.exe') if "Microsoft (R) Fortran PowerStation" in output: return True else: return False def get_line(file: Path, line: int): with open(file, 'r', encoding='utf-8') as f: return f.readlines()[line-1] @app.command() def build( input_files: list[Path] = typer.Argument(help="List of source files and libraries"), output: Path = typer.Option( help="Output executable", default=Path("app.exe")), run: Annotated[ bool, typer.Option(help="Run the executable after compilation") ] = False, friendly: Annotated[bool, typer.Option(help="Convert fortran numbers to friendly format")] = False, clean: bool = typer.Option(help="Clean build folder after compilation", default=True), pause: bool = typer.Option(help="Pause after compilation", default=False), ): """ Build executable from source files and libraries. """ if not check_fl32_output(): errexit('Microsoft Fortran PowerStation fl32.exe compiler is not installed. Please install it and try again.') tmp_folder = Path("build/") if tmp_folder.exists(): shutil.rmtree(tmp_folder) os.mkdir(tmp_folder) if not input_files: errexit("No input files were specified.") for file in input_files: if not file.exists(): errexit(f'File "{file}" was not found in current directory.') f90_files = [file for file in input_files if file.suffix.lower() == ".f90"] lib_files = [str(file) for file in input_files if file.suffix.lower() == ".lib"] info(f'Source files to be compiled: {", ".join(map(str, f90_files))}') if lib_files: info(f'Libraries to be linked: {", ".join(lib_files)}') info(f'Output executable: "{output}"') info("Compiling files...") stats = {} for f90_file in f90_files: compile_output = safe_check_output(f'fl32 /c /Fo"build\\{f90_file.stem}" /nologo {f90_file}') failed = False for line in compile_output.split('\n'): match = re.fullmatch('(.+)\(([0-9]+)\): (\S+) (\S+): (.+)', line) if match: file, line, info_type, code, message = match.groups() if info_type == 'error': typer.echo(typer.style(f"\nerror: ", fg=typer.colors.RED) + f'{message}, file {file} at line {line} [{code}]') fl = get_line(Path(file), int(line)).strip() typer.echo(f'{line} {fl}') typer.echo(typer.style(f'{" " * len(line)} {"^" * len(fl)}\n', fg=typer.colors.RED)) failed = True stats['errors'] = stats.get('errors', 0) + 1 if info_type == 'warning': typer.echo(typer.style(f"\nwarning: ", fg=typer.colors.YELLOW) + f'{message}, file {file} at line {line} [{code}]') fl = get_line(Path(file), int(line)).strip() typer.echo(f'{line} {fl}') typer.echo(typer.style(f'{" " * len(line)} {"^" * len(fl)}\n', fg=typer.colors.YELLOW)) stats['warnings'] = stats.get('warnings', 0) + 1 if failed: errexit(f'Failed to compile "{f90_file}", errors listed above. Check your source code and try again.') info("Linking to executable...") joined_files_with_dist = [ str(Path("build") / (file.stem + ".obj")) for file in f90_files ] try_or_exit( f'fl32 /nologo {" ".join(joined_files_with_dist)} {" ".join(lib_files)} /Fe"{output}"' ) if clean: shutil.rmtree(tmp_folder) info(f"Compilation finished to '{output}'. Errors: {stats.get('errors', 0)}, Warnings: {stats.get('warnings', 0)}") if run: info(f'Executing "{output}"...') hint("If nothing happens, press ENTER to continue...") if not friendly: process = Popen([output], stdout=PIPE) (output, _) = process.communicate() _ = process.wait() output = output.decode("cp866") print(output) if pause: input('Press ENTER to continue... ') sys.exit(0) info('Running in friendly mode! :)') process = Popen([output], stdout=PIPE) (output, _) = process.communicate() _ = process.wait() output = output.decode("cp866") for match in re.findall("(\.(\d+)D\+(\d+))", output): number = float(f"0.{match[1]}") for _ in range(int(match[2].strip("0") or "0")): number *= 10 output = output.replace(match[0], format_number(number)) for match in re.findall("(\.(\d+)D\-(\d+))", output): number = float(f"0.{match[1]}") for _ in range(int(match[2].strip("0") or "0")): number /= 10 output = output.replace(match[0], format_number(number)) print(output) if pause: input('Press ENTER to continue... ') @app.command() def version(): """ Prints the version of the tool. """ print( "Developed for Moscow Aviation University (MAU) for building legacy Fortran projects with ease. Special for 609 students with love." ) print_header() app()