2024-07-26 16:55:21 +05:30
|
|
|
#!/usr/bin/env python3
|
2023-11-17 00:31:41 +05:30
|
|
|
|
2024-06-02 22:03:30 +05:30
|
|
|
if __name__ == "__main__":
|
|
|
|
print(":: C bytebeat generator: compiler unit")
|
|
|
|
|
2023-11-17 00:59:52 +05:30
|
|
|
from argparse import ArgumentParser
|
2024-09-23 03:13:25 +05:30
|
|
|
from decimal import Decimal
|
|
|
|
from math import ceil
|
2024-08-27 01:56:59 +05:30
|
|
|
from os import getcwd, environ, makedirs, name as os_name, rename
|
2024-08-27 01:14:58 +05:30
|
|
|
from os.path import exists, join as path_join
|
2024-08-26 20:12:36 +05:30
|
|
|
from shlex import join as command_line_join, split as command_line_split
|
2024-06-02 22:04:49 +05:30
|
|
|
from shutil import which
|
2024-08-26 21:56:00 +05:30
|
|
|
from sys import stdin, stdout
|
2024-08-27 01:04:39 +05:30
|
|
|
from tempfile import TemporaryDirectory
|
2024-01-09 21:05:28 +05:30
|
|
|
from typing import Dict, Union
|
2023-11-17 00:59:52 +05:30
|
|
|
import subprocess
|
|
|
|
|
2024-07-14 02:43:25 +05:30
|
|
|
# Definitions
|
|
|
|
BITS_PER_BYTE = 8
|
2024-08-26 19:59:17 +05:30
|
|
|
EXIT_FAILURE = 1
|
|
|
|
EXIT_SUCCESS = 0
|
2024-07-14 02:43:25 +05:30
|
|
|
|
2023-11-17 00:59:52 +05:30
|
|
|
# Paths
|
|
|
|
PATHS = {
|
2024-01-02 20:43:28 +05:30
|
|
|
"src_dir": "src",
|
2024-08-20 21:16:47 +05:30
|
|
|
"bin_dir": "bin",
|
2023-11-17 00:59:52 +05:30
|
|
|
"template": "template.c",
|
|
|
|
"substitute": "substituted.c",
|
2024-09-23 00:29:11 +05:30
|
|
|
"executable": "render_bytebeat",
|
2023-12-30 18:01:11 +05:30
|
|
|
"fwrite_le_header": "fwrite_le.h",
|
2024-05-19 15:04:15 +05:30
|
|
|
"fwrite_le": "fwrite_le.c",
|
|
|
|
"include_directory": "include"
|
2023-11-17 00:59:52 +05:30
|
|
|
}
|
|
|
|
|
2024-08-27 01:56:59 +05:30
|
|
|
# Add current directory before all paths for compilation
|
|
|
|
CURRENT_DIRECTORY = getcwd()
|
2024-08-27 01:24:02 +05:30
|
|
|
for key in ["src_dir", "bin_dir", "include_directory"]:
|
2024-08-27 01:56:59 +05:30
|
|
|
PATHS[key] = path_join(CURRENT_DIRECTORY, PATHS[key])
|
2024-08-27 01:24:02 +05:30
|
|
|
|
2024-05-19 15:05:07 +05:30
|
|
|
# Resolve paths
|
2023-11-17 00:59:52 +05:30
|
|
|
PATHS["template"] = path_join(PATHS["src_dir"], PATHS["template"])
|
2024-08-27 01:04:39 +05:30
|
|
|
PATHS["substitute_kept"] = path_join(PATHS["bin_dir"], PATHS["substitute"])
|
2024-09-23 00:29:11 +05:30
|
|
|
PATHS["executable_kept"] = path_join(PATHS["bin_dir"], PATHS["executable"])
|
2024-01-02 20:43:28 +05:30
|
|
|
PATHS["fwrite_le"] = path_join(PATHS["src_dir"], PATHS["fwrite_le"])
|
2023-11-17 00:59:52 +05:30
|
|
|
|
|
|
|
# Default parameters
|
2023-11-17 00:31:41 +05:30
|
|
|
DEFAULT_PARAMETERS = {
|
2024-04-14 04:08:48 +05:30
|
|
|
"CC": "cc",
|
2024-04-14 14:25:04 +05:30
|
|
|
"CFLAGS": "-Ofast -march=native -mtune=native -Wall -Wextra -Wpedantic "
|
|
|
|
"-pedantic -Wno-unused-variable -Wno-unused-but-set-variable "
|
2024-08-27 01:14:58 +05:30
|
|
|
"-Wno-dangling-else -Wno-parentheses -std=c99"
|
2023-11-17 00:31:41 +05:30
|
|
|
}
|
|
|
|
|
2024-05-19 23:08:06 +05:30
|
|
|
stdout_atty = hasattr(stdout, "isatty") and stdout.isatty()
|
|
|
|
|
2023-11-17 00:31:41 +05:30
|
|
|
def fetch(name: str):
|
2023-12-24 21:50:47 +05:30
|
|
|
if from_env := environ.get(name):
|
2024-01-07 04:50:34 +05:30
|
|
|
return from_env
|
2024-01-10 01:39:02 +05:30
|
|
|
elif name != "CFLAGS_EXTRA":
|
2024-01-07 04:50:34 +05:30
|
|
|
return DEFAULT_PARAMETERS[name]
|
2023-11-17 00:31:41 +05:30
|
|
|
|
|
|
|
def read_file(path: str) -> str:
|
|
|
|
return open(path, "r", encoding="utf-8-sig").read()
|
|
|
|
|
2024-08-27 02:01:30 +05:30
|
|
|
def overwrite_file(path: str, content: str) -> int:
|
2024-01-09 18:24:00 +05:30
|
|
|
return open(path, "w", encoding="utf-8").write(content)
|
2023-11-17 00:31:41 +05:30
|
|
|
|
|
|
|
def read_from_file_or_stdin(path: str) -> str:
|
|
|
|
if path == "-":
|
2024-08-26 20:08:58 +05:30
|
|
|
print("Reading from STDIN...", flush=True)
|
2023-11-17 00:31:41 +05:30
|
|
|
return "\n".join(stdin)
|
|
|
|
elif exists(path):
|
|
|
|
return read_file(path)
|
|
|
|
else:
|
2024-08-26 21:56:00 +05:30
|
|
|
raise SystemExit(f"The specified file {path} doesn't exist")
|
2023-11-17 00:31:41 +05:30
|
|
|
|
2024-01-09 21:05:28 +05:30
|
|
|
def substitute_vars(replacements: Dict[str, Union[bool, str]], text: str,
|
|
|
|
verbose: bool) -> str:
|
2024-01-09 20:34:26 +05:30
|
|
|
if verbose:
|
|
|
|
print("Substituting values:")
|
2023-12-03 18:25:08 +05:30
|
|
|
for placeholder, replacement in replacements.items():
|
2024-01-09 21:04:12 +05:30
|
|
|
if isinstance(replacement, bool):
|
|
|
|
replacement = preprocessor_bool(replacement)
|
|
|
|
|
2024-01-09 20:34:26 +05:30
|
|
|
if verbose and placeholder != "bytebeat_contents":
|
|
|
|
print(placeholder, ": ", replacement, sep="")
|
2023-12-03 18:25:08 +05:30
|
|
|
text = text.replace(f"`{placeholder}`", str(replacement))
|
2024-01-09 20:50:06 +05:30
|
|
|
if verbose:
|
|
|
|
print()
|
2023-12-03 18:25:08 +05:30
|
|
|
return text
|
2023-11-17 00:31:41 +05:30
|
|
|
|
2024-09-23 02:29:41 +05:30
|
|
|
def run_command(*command: list[str], stage: str) -> None:
|
2024-08-27 01:58:52 +05:30
|
|
|
print("[>]", command_line_join(command), flush=True)
|
2024-08-26 20:06:09 +05:30
|
|
|
if subprocess.run(command).returncode != EXIT_SUCCESS:
|
2024-09-23 02:29:41 +05:30
|
|
|
if stage == "rendering":
|
|
|
|
print("An error occured during rendering!")
|
|
|
|
|
2024-08-26 21:56:00 +05:30
|
|
|
raise SystemExit(EXIT_FAILURE)
|
2024-08-26 20:06:09 +05:30
|
|
|
|
2024-09-23 00:29:11 +05:30
|
|
|
def compile_substituted_file(input_file: str, executable_file: str) -> None:
|
2024-08-27 01:28:40 +05:30
|
|
|
print("Compiling")
|
|
|
|
|
|
|
|
run_command(
|
|
|
|
CC,
|
|
|
|
*command_line_split(CFLAGS),
|
|
|
|
input_file,
|
|
|
|
PATHS["fwrite_le"],
|
2024-09-23 00:29:11 +05:30
|
|
|
"-o", executable_file,
|
2024-09-23 02:29:41 +05:30
|
|
|
"-I" + PATHS["include_directory"],
|
|
|
|
stage="compilation"
|
2024-08-27 01:28:40 +05:30
|
|
|
)
|
2024-09-23 02:29:41 +05:30
|
|
|
run_command(executable_file, stage="rendering")
|
2024-08-27 01:28:40 +05:30
|
|
|
|
2024-09-23 00:29:11 +05:30
|
|
|
def main_workflow(input_file: str, executable_file: str, \
|
2024-08-27 01:28:40 +05:30
|
|
|
substitute_contents: Dict[str, str]) -> None:
|
2024-08-27 02:01:30 +05:30
|
|
|
overwrite_file(input_file, substitute_contents)
|
2024-09-23 00:29:11 +05:30
|
|
|
compile_substituted_file(input_file, executable_file)
|
2024-08-27 01:28:40 +05:30
|
|
|
|
2024-01-09 20:50:27 +05:30
|
|
|
preprocessor_bool = lambda value: "1" if value else "0"
|
2024-01-09 22:34:33 +05:30
|
|
|
C_str_repr = lambda s: '"' + s.replace("\\", "\\\\").replace(r'"', r'\"') + '"'
|
2024-01-09 20:50:27 +05:30
|
|
|
|
2023-11-17 00:31:41 +05:30
|
|
|
CC = fetch("CC")
|
2024-06-02 22:04:49 +05:30
|
|
|
|
|
|
|
CC_SEARCH_LIST = [
|
|
|
|
"gcc",
|
|
|
|
"clang",
|
|
|
|
"tcc"
|
|
|
|
]
|
|
|
|
if os_name == "nt":
|
|
|
|
CC_SEARCH_LIST = [
|
|
|
|
"msc",
|
|
|
|
*CC_SEARCH_LIST
|
|
|
|
]
|
|
|
|
|
2023-12-30 17:27:32 +05:30
|
|
|
CFLAGS = fetch("CFLAGS")
|
2024-01-10 01:39:02 +05:30
|
|
|
if extra := fetch("CFLAGS_EXTRA"):
|
|
|
|
CFLAGS += " " + extra
|
2023-11-17 00:31:41 +05:30
|
|
|
|
2024-06-02 22:04:49 +05:30
|
|
|
is_cmd_available = lambda cmd: which(cmd) is not None
|
|
|
|
is_cmd_unavailable = lambda cmd: which(cmd) is None
|
|
|
|
|
2023-11-17 00:31:41 +05:30
|
|
|
if __name__ == "__main__":
|
|
|
|
parser = ArgumentParser(description=\
|
|
|
|
"Substitutes supplied C (non-JavaScript!) bytebeat into the template, "
|
2024-01-09 19:18:34 +05:30
|
|
|
"then attempts to compile the instance of the template. Accepts "
|
2024-08-27 01:14:58 +05:30
|
|
|
"environmental variables `CC`, `CFLAGS`. `CFLAGS_EXTRA` can be used to "
|
|
|
|
"add to default `CFLAGS`.")
|
2023-11-17 00:31:41 +05:30
|
|
|
parser.add_argument("file", type=str,
|
2023-12-03 18:14:52 +05:30
|
|
|
help="bytebeat formula file (use `-` to read from stdin)")
|
2024-01-09 22:34:33 +05:30
|
|
|
parser.add_argument("-o", "--output", default="output.wav", type=str,
|
2024-09-23 00:23:04 +05:30
|
|
|
help="specify output WAVE file path (default is `output.wav`")
|
2023-11-17 00:31:41 +05:30
|
|
|
parser.add_argument("-r", "--sample-rate", default=8000, type=int,
|
|
|
|
help="sample rate (Hz)")
|
2023-11-18 16:45:48 +05:30
|
|
|
parser.add_argument("-p", "--final-sample-rate", default=None, type=int,
|
|
|
|
help="convert the output to a different sample rate (usually one that "
|
|
|
|
"is set in the system, to improve sound quality) during generation "
|
|
|
|
"(not just reinterpretation)")
|
2023-11-17 00:31:41 +05:30
|
|
|
parser.add_argument("-b", "--bit-depth", default=8, type=int,
|
|
|
|
help="bit depth")
|
|
|
|
parser.add_argument("-s", "--signed", default=False, action="store_true",
|
|
|
|
help="is signed?")
|
2024-01-09 20:44:32 +05:30
|
|
|
parser.add_argument("-R", "--precalculate-ratio", default=False,
|
|
|
|
action="store_true",
|
|
|
|
help="precalculate sample ratio to speed up rendering (may produce "
|
|
|
|
"inaccurate results")
|
|
|
|
parser.add_argument("-m", "--faster-sample-ratio-math", default=False,
|
|
|
|
action="store_true",
|
|
|
|
help="faster sample ratio math (implies argument -R)")
|
2024-01-09 20:05:27 +05:30
|
|
|
parser.add_argument("-f", "--floating-point", default=False,
|
|
|
|
action="store_true", help="use floating point as the return type")
|
2023-11-17 00:31:41 +05:30
|
|
|
parser.add_argument("-c", "--channels", default=1, type=int,
|
|
|
|
help="amount of channels")
|
2024-09-23 03:13:25 +05:30
|
|
|
parser.add_argument("-t", "--seconds", default=None, type=Decimal,
|
2023-12-03 18:13:40 +05:30
|
|
|
help="length in seconds (samples = sample rate * seconds) : "
|
2024-01-09 21:06:27 +05:30
|
|
|
"default = 30 seconds")
|
2023-12-03 18:13:40 +05:30
|
|
|
parser.add_argument("-l", "--samples", default=None, type=int,
|
|
|
|
help="length in samples (adds to `-t`; supports negative numbers) : "
|
|
|
|
"default = +0 samples")
|
2024-09-23 03:51:13 +05:30
|
|
|
parser.add_argument("-a", "--custom-return-code", default=False,
|
|
|
|
action="store_true",
|
2023-11-17 00:31:41 +05:30
|
|
|
help="do not insert return statement before the code")
|
2024-01-10 06:37:15 +05:30
|
|
|
parser.add_argument("-u", "--mode", default="sequential", type=str,
|
|
|
|
help="mode of writing: `sequential` or `instant` (the latter is not "
|
|
|
|
"recommended, since the whole result would be stored in RAM)")
|
|
|
|
parser.add_argument("-n", "--block-size", default=65536, type=int,
|
|
|
|
help="sequential mode only: block size of each sequence, bytes")
|
2023-11-25 23:04:11 +05:30
|
|
|
parser.add_argument("-q", "--silent", default=False, action="store_true",
|
|
|
|
help="do not output anything during generation")
|
2023-11-25 23:33:27 +05:30
|
|
|
parser.add_argument("-v", "--verbose", default=False, action="store_true",
|
2023-11-25 23:36:19 +05:30
|
|
|
help="show progress during generation")
|
2024-01-09 20:34:26 +05:30
|
|
|
parser.add_argument("-E", "--show-substituted-values", default=False,
|
|
|
|
action="store_true", help="show substituted values")
|
2024-05-19 23:08:06 +05:30
|
|
|
parser.add_argument("--color", default="auto", type=str,
|
|
|
|
help="ANSI escape codes. Set to 'always' to enable them, 'none' to "
|
|
|
|
"disable. Default: 'auto'.")
|
2024-08-27 00:10:33 +05:30
|
|
|
parser.add_argument("--keep-files", default=False, action="store_true",
|
|
|
|
help="keep generated files: substituted source code of runtime unit "
|
|
|
|
"and the executable it will be compiled to.")
|
2023-11-17 00:31:41 +05:30
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
bytebeat_contents = read_from_file_or_stdin(args.file).strip()
|
|
|
|
|
|
|
|
if not bytebeat_contents:
|
2024-08-27 00:09:36 +05:30
|
|
|
raise SystemExit("Empty file or STDIN")
|
2023-11-17 00:31:41 +05:30
|
|
|
|
2023-11-17 00:59:52 +05:30
|
|
|
# - Compilation
|
2024-09-23 03:51:13 +05:30
|
|
|
if not args.custom_return_code: # Insert `return` statement
|
2024-04-14 14:59:12 +05:30
|
|
|
# XXX: The bytebeat code is enclosed in parentheses to allow for the
|
|
|
|
# use of commas as a comma operator, enabling more formulas to function.
|
|
|
|
bytebeat_contents = f"return ({bytebeat_contents})"
|
2023-11-17 00:31:41 +05:30
|
|
|
|
2024-01-09 20:44:32 +05:30
|
|
|
original_sample_rate = args.sample_rate
|
2023-11-18 16:45:48 +05:30
|
|
|
final_sample_rate_code = ""
|
2024-01-09 20:44:32 +05:30
|
|
|
if args.faster_sample_ratio_math:
|
|
|
|
args.precalculate_ratio = True
|
2024-01-10 01:49:38 +05:30
|
|
|
if args.precalculate_ratio and not args.final_sample_rate is None:
|
2024-01-09 20:44:32 +05:30
|
|
|
if args.faster_sample_ratio_math:
|
|
|
|
sample_rate_ratio = args.sample_rate / args.final_sample_rate
|
2024-05-19 23:14:23 +05:30
|
|
|
final_sample_rate_code = f"time *= {sample_rate_ratio}L;"
|
2024-01-09 20:44:32 +05:30
|
|
|
else:
|
|
|
|
sample_rate_ratio = args.final_sample_rate / args.sample_rate
|
2024-05-19 23:14:23 +05:30
|
|
|
final_sample_rate_code = f"time /= {sample_rate_ratio}L;"
|
2023-11-18 16:45:48 +05:30
|
|
|
args.sample_rate = args.final_sample_rate
|
|
|
|
|
2024-01-12 01:33:45 +05:30
|
|
|
final_sample_rate = \
|
|
|
|
value if (value := args.final_sample_rate) else original_sample_rate
|
2023-12-03 18:13:40 +05:30
|
|
|
samples = 0
|
|
|
|
while True:
|
|
|
|
no_seconds = args.seconds is None or args.seconds == 0
|
|
|
|
no_samples = args.samples is None or args.samples == 0
|
2024-01-12 23:22:12 +05:30
|
|
|
seconds_specified = not no_seconds
|
|
|
|
samples_specified = not no_samples
|
2023-12-03 18:13:40 +05:30
|
|
|
|
2024-01-12 23:22:12 +05:30
|
|
|
if seconds_specified and args.seconds < 0:
|
2024-08-26 21:56:00 +05:30
|
|
|
raise SystemExit("CLI: Count of seconds can't be less than zero.")
|
2023-12-03 18:13:40 +05:30
|
|
|
|
2024-01-12 23:22:12 +05:30
|
|
|
if no_seconds and samples_specified:
|
2023-12-03 18:13:40 +05:30
|
|
|
samples = args.samples
|
2024-01-12 23:22:12 +05:30
|
|
|
elif seconds_specified and samples_specified:
|
2024-01-12 01:33:45 +05:30
|
|
|
samples = args.seconds * final_sample_rate + args.samples
|
2024-01-12 23:22:12 +05:30
|
|
|
elif seconds_specified and no_samples:
|
2024-01-12 01:33:45 +05:30
|
|
|
samples = args.seconds * final_sample_rate
|
2023-12-03 18:13:40 +05:30
|
|
|
elif no_seconds and no_samples:
|
|
|
|
args.seconds = 30 # default
|
|
|
|
continue
|
|
|
|
else:
|
2024-08-26 21:56:00 +05:30
|
|
|
raise SystemExit("CLI: Incorrect seconds/samples length format.")
|
2023-12-03 18:13:40 +05:30
|
|
|
break
|
|
|
|
|
|
|
|
if samples <= 0:
|
2024-08-26 21:56:00 +05:30
|
|
|
raise SystemExit("CLI: Count of samples should be greater than zero.")
|
2023-12-03 18:13:40 +05:30
|
|
|
|
2024-09-23 03:13:25 +05:30
|
|
|
# round the number of samples
|
|
|
|
samples = ceil(samples)
|
|
|
|
|
2024-01-10 06:37:15 +05:30
|
|
|
if args.mode != "sequential" and args.mode != "instant":
|
2024-08-26 21:56:00 +05:30
|
|
|
raise SystemExit(f"Invalid mode '{args.mode}'")
|
2024-01-10 06:37:15 +05:30
|
|
|
|
2024-05-19 14:11:51 +05:30
|
|
|
gen_length = args.channels * samples
|
|
|
|
|
2024-05-19 23:08:06 +05:30
|
|
|
ansi_escape_codes_supported = args.color == "auto" and stdout_atty or \
|
|
|
|
args.color == "always"
|
|
|
|
|
2024-06-02 22:04:49 +05:30
|
|
|
if is_cmd_unavailable(CC):
|
2024-08-27 00:09:36 +05:30
|
|
|
print(f"Compiler {CC} is not available, searching:")
|
2024-06-02 22:04:49 +05:30
|
|
|
|
|
|
|
still_unavailable = True
|
|
|
|
for compiler in CC_SEARCH_LIST:
|
|
|
|
print(f"* Trying CC={compiler}", end="")
|
|
|
|
if is_cmd_available(compiler):
|
|
|
|
print(": OK")
|
|
|
|
CC = compiler
|
|
|
|
still_unavailable = False
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
print()
|
|
|
|
|
|
|
|
if still_unavailable:
|
2024-08-26 21:56:00 +05:30
|
|
|
raise SystemExit("Could not find an available compiler. Please "
|
2024-08-27 00:09:36 +05:30
|
|
|
"specify it by setting\nan environmental variable "
|
2024-08-26 21:56:00 +05:30
|
|
|
"CC.")
|
2024-06-02 22:04:49 +05:30
|
|
|
|
2024-08-27 01:28:40 +05:30
|
|
|
substitute_contents = substitute_vars({
|
|
|
|
"bytebeat_contents": bytebeat_contents,
|
|
|
|
"output_file": C_str_repr(args.output),
|
|
|
|
"sample_rate": \
|
|
|
|
value if (value := args.final_sample_rate) else args.sample_rate,
|
|
|
|
"original_sample_rate": original_sample_rate,
|
|
|
|
"final_sample_rate_code": final_sample_rate_code,
|
|
|
|
"bit_depth": args.bit_depth,
|
|
|
|
"is_signed": args.signed,
|
|
|
|
"precalculated_ratio": args.precalculate_ratio,
|
|
|
|
"faster_sample_ratio_math": args.precalculate_ratio,
|
|
|
|
"fp_return_type": args.floating_point,
|
|
|
|
"channels": args.channels,
|
|
|
|
"length": samples,
|
|
|
|
"wav_product": gen_length * (args.bit_depth // BITS_PER_BYTE),
|
|
|
|
"gen_length": gen_length,
|
|
|
|
"sequential_mode": args.mode == "sequential",
|
|
|
|
"block_size": args.block_size,
|
|
|
|
"silent_mode": args.silent,
|
|
|
|
"verbose_mode": args.verbose and not args.silent,
|
|
|
|
"fwrite_le": PATHS["fwrite_le_header"],
|
|
|
|
"ansi_escape_codes_supported": ansi_escape_codes_supported
|
|
|
|
}, read_file(PATHS["template"]), args.show_substituted_values)
|
|
|
|
|
|
|
|
if args.keep_files:
|
|
|
|
makedirs(PATHS["bin_dir"], exist_ok=True)
|
|
|
|
|
|
|
|
substitute_file = PATHS["substitute_kept"]
|
2024-09-23 00:29:11 +05:30
|
|
|
executable_file = PATHS["executable_kept"]
|
2024-08-27 01:28:40 +05:30
|
|
|
|
2024-09-23 00:29:11 +05:30
|
|
|
main_workflow(substitute_file, executable_file, substitute_contents)
|
2024-08-27 01:28:40 +05:30
|
|
|
else:
|
|
|
|
with TemporaryDirectory() as tmpdirname:
|
|
|
|
temporary_path = lambda path: path_join(tmpdirname, path)
|
|
|
|
|
|
|
|
substitute_temp = temporary_path(PATHS["substitute"])
|
2024-09-23 00:29:11 +05:30
|
|
|
executable_temp = temporary_path(PATHS["executable"])
|
2024-08-27 01:28:40 +05:30
|
|
|
|
2024-09-23 00:29:11 +05:30
|
|
|
main_workflow(substitute_temp, executable_temp, substitute_contents)
|