fortrun/main.py
2024-09-03 19:19:33 +03:00

208 lines
7.0 KiB
Python

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()