Add black
This commit is contained in:
parent
d352e70956
commit
b0d801f8a1
14
lmm/cli.py
14
lmm/cli.py
@ -9,6 +9,7 @@ from lmm.mods.bg3 import install_mod as bg3_install_mod
|
||||
|
||||
import click
|
||||
|
||||
|
||||
def get_game_by_name(name: str) -> Optional[Game]:
|
||||
games = load_game_configs()
|
||||
for game in games:
|
||||
@ -16,12 +17,14 @@ def get_game_by_name(name: str) -> Optional[Game]:
|
||||
return game
|
||||
return None
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.pass_context
|
||||
def cli(ctx):
|
||||
if ctx.invoked_subcommand is None:
|
||||
click.echo("Invoked without a subcommand")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("-g", "--game", required=True)
|
||||
@click.option("-p", "--profile", required=True)
|
||||
@ -67,6 +70,7 @@ def launch(game: str, profile: str):
|
||||
for mount in mounts:
|
||||
mount.unmount()
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("-g", "--game", required=True)
|
||||
@click.option("-P", "--path", required=True)
|
||||
@ -75,11 +79,12 @@ def install(game: str, path: str):
|
||||
BG3: bg3_install_mod,
|
||||
}.get(game, None)
|
||||
if install is None:
|
||||
click.echo(f"Unknown game \"{game}\"")
|
||||
click.echo(f'Unknown game "{game}"')
|
||||
return
|
||||
|
||||
install(Path(path))
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("-g", "--game")
|
||||
def mods(game: Optional[str]):
|
||||
@ -87,7 +92,9 @@ def mods(game: Optional[str]):
|
||||
games = [game]
|
||||
else:
|
||||
games = [
|
||||
item for item in os.listdir(LMM_GAMES_PATH) if (LMM_GAMES_PATH / item).is_dir()
|
||||
item
|
||||
for item in os.listdir(LMM_GAMES_PATH)
|
||||
if (LMM_GAMES_PATH / item).is_dir()
|
||||
]
|
||||
|
||||
for idx, game_name in enumerate(games):
|
||||
@ -102,6 +109,7 @@ def mods(game: Optional[str]):
|
||||
if idx < len(games) - 1:
|
||||
click.echo()
|
||||
|
||||
|
||||
@cli.command
|
||||
def list():
|
||||
games = load_game_configs()
|
||||
@ -130,8 +138,10 @@ def list():
|
||||
if idx < len(games) - 1:
|
||||
click.echo()
|
||||
|
||||
|
||||
def main():
|
||||
cli()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
@ -3,6 +3,7 @@ import subprocess
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_cmd(args: list[str], cd: Optional[Path] = None) -> int:
|
||||
cmdline = " ".join(args)
|
||||
if cd is not None:
|
||||
@ -16,17 +17,20 @@ def run_cmd(args: list[str], cd: Optional[Path] = None) -> int:
|
||||
os.chdir(cwd)
|
||||
return ret
|
||||
|
||||
|
||||
def run_sudo_cmd(args: list[str], cd: Optional[Path] = None) -> int:
|
||||
return run_cmd(
|
||||
[
|
||||
"pkexec",
|
||||
"--user", "root",
|
||||
"--user",
|
||||
"root",
|
||||
"--keep-cwd",
|
||||
*args,
|
||||
],
|
||||
cd=cd,
|
||||
)
|
||||
|
||||
|
||||
def run_cmd_shell(cmd: str) -> None:
|
||||
print("Executing:", cmd)
|
||||
subprocess.run(
|
||||
@ -34,6 +38,7 @@ def run_cmd_shell(cmd: str) -> None:
|
||||
shell=True,
|
||||
)
|
||||
|
||||
|
||||
def run_cmd_nonblocking(args: list[str]) -> subprocess.Popen:
|
||||
cmdline = " ".join(args)
|
||||
print("Executing:", cmdline)
|
||||
|
@ -12,6 +12,7 @@ from lmm.runners.base import runner_from_config
|
||||
|
||||
GAME_NAME = "BaldursGate3"
|
||||
|
||||
|
||||
class BaldursGate3ModType(Enum):
|
||||
# Override the game files
|
||||
ROOT = 1
|
||||
@ -19,6 +20,7 @@ class BaldursGate3ModType(Enum):
|
||||
# Override the Paks in the compatdata prefix
|
||||
PAK = 2
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaldursGate3Mod:
|
||||
# The directory name
|
||||
@ -36,7 +38,9 @@ class BaldursGate3Profile(Profile):
|
||||
super().__init__(GAME_NAME, name, **kwargs)
|
||||
self.mods = mods
|
||||
|
||||
def get_mods(self, type: BaldursGate3ModType) -> Generator[BaldursGate3Mod, None, None]:
|
||||
def get_mods(
|
||||
self, type: BaldursGate3ModType
|
||||
) -> Generator[BaldursGate3Mod, None, None]:
|
||||
"""Yields all mods of type @type"""
|
||||
for mod in self.mods:
|
||||
if not mod.type == type:
|
||||
@ -51,6 +55,7 @@ class BaldursGate3Profile(Profile):
|
||||
def get_mod_names(self) -> list[str]:
|
||||
return [mod.name for mod in self.mods]
|
||||
|
||||
|
||||
class BaldursGate3Game(ProtonGame):
|
||||
def __init__(self, profiles: list[BaldursGate3Profile], **kwargs):
|
||||
super().__init__(GAME_NAME, "1086940", profiles, **kwargs)
|
||||
@ -65,7 +70,18 @@ class BaldursGate3Game(ProtonGame):
|
||||
|
||||
@property
|
||||
def user_mods_path(self) -> Path:
|
||||
return self.compat_path / "pfx" / "drive_c" / "users" / "steamuser" / "AppData" / "Local" / "Larian Studios" / "Baldur's Gate 3" / "Mods"
|
||||
return (
|
||||
self.compat_path
|
||||
/ "pfx"
|
||||
/ "drive_c"
|
||||
/ "users"
|
||||
/ "steamuser"
|
||||
/ "AppData"
|
||||
/ "Local"
|
||||
/ "Larian Studios"
|
||||
/ "Baldur's Gate 3"
|
||||
/ "Mods"
|
||||
)
|
||||
|
||||
def can_start(self) -> bool:
|
||||
return self.user_mods_path.exists() and self.installation_path.exists()
|
||||
@ -87,9 +103,7 @@ class BaldursGate3Game(ProtonGame):
|
||||
|
||||
if profile.has_mods_of_type(BaldursGate3ModType.ROOT):
|
||||
mods = list(profile.get_mods(BaldursGate3ModType.ROOT))
|
||||
mod_paths = [
|
||||
LMM_GAMES_PATH / GAME_NAME / mod.name for mod in mods
|
||||
]
|
||||
mod_paths = [LMM_GAMES_PATH / GAME_NAME / mod.name for mod in mods]
|
||||
overlays.append(
|
||||
OverlayFSMount(
|
||||
self.installation_path,
|
||||
@ -102,9 +116,7 @@ class BaldursGate3Game(ProtonGame):
|
||||
print(f"Loaded root mods: {mod_names}")
|
||||
if profile.has_mods_of_type(BaldursGate3ModType.PAK):
|
||||
mods = list(profile.get_mods(BaldursGate3ModType.PAK))
|
||||
mod_paths = [
|
||||
os.path.join("./", mod.name) for mod in mods
|
||||
]
|
||||
mod_paths = [os.path.join("./", mod.name) for mod in mods]
|
||||
|
||||
# Merge all mods
|
||||
overlays.append(
|
||||
@ -134,9 +146,11 @@ class BaldursGate3Game(ProtonGame):
|
||||
{
|
||||
"name": mod.name,
|
||||
"type": mod.type.value,
|
||||
} for mod in profile.mods
|
||||
]
|
||||
} for profile in self.profiles
|
||||
}
|
||||
for mod in profile.mods
|
||||
],
|
||||
}
|
||||
for profile in self.profiles
|
||||
]
|
||||
}
|
||||
|
||||
@ -149,7 +163,9 @@ class BaldursGate3Game(ProtonGame):
|
||||
mods.append(
|
||||
BaldursGate3Mod(
|
||||
mod["name"],
|
||||
BaldursGate3ModType.ROOT if mod["type"] == 1 else BaldursGate3ModType.PAK,
|
||||
BaldursGate3ModType.ROOT
|
||||
if mod["type"] == 1
|
||||
else BaldursGate3ModType.PAK,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -9,9 +9,11 @@ from lmm.overlayfs import OverlayFSMount
|
||||
from lmm.profile import Profile
|
||||
from lmm.steam import find_library_folder_for_game
|
||||
|
||||
|
||||
class CannotStartReason:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class PathNotExistingReason(CannotStartReason):
|
||||
# The path that is not existing
|
||||
@ -20,6 +22,7 @@ class PathNotExistingReason(CannotStartReason):
|
||||
def __str__(self) -> str:
|
||||
return f"Path {self.path} does not exist"
|
||||
|
||||
|
||||
class Game(abc.ABC):
|
||||
# The name of the game.
|
||||
name: str
|
||||
@ -30,7 +33,9 @@ class Game(abc.ABC):
|
||||
# The default runner to use when launching the game.
|
||||
default_runner: Optional["Runner"]
|
||||
|
||||
def __init__(self, name: str, profiles: list[Profile], default_runner: Optional["Runner"]):
|
||||
def __init__(
|
||||
self, name: str, profiles: list[Profile], default_runner: Optional["Runner"]
|
||||
):
|
||||
self.name = name
|
||||
self.profiles = profiles
|
||||
self.default_runner = default_runner
|
||||
@ -63,6 +68,7 @@ class Game(abc.ABC):
|
||||
def wait(self):
|
||||
return None
|
||||
|
||||
|
||||
class ProtonGame(Game, abc.ABC):
|
||||
# The appid of the game in Steam.
|
||||
appid: str
|
||||
@ -70,7 +76,13 @@ class ProtonGame(Game, abc.ABC):
|
||||
# The PID of the game.
|
||||
_pid: Optional[str]
|
||||
|
||||
def __init__(self, name: str, appid: str, profiles: list[Profile], default_runner: Optional["Runner"]):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
appid: str,
|
||||
profiles: list[Profile],
|
||||
default_runner: Optional["Runner"],
|
||||
):
|
||||
super().__init__(name, profiles, default_runner)
|
||||
self._pid = None
|
||||
self.appid = appid
|
||||
@ -106,7 +118,9 @@ class ProtonGame(Game, abc.ABC):
|
||||
# Workaround for games like Baldur's Gate 3 that have a relative path in the cmdline.
|
||||
# Note that the cmdline contains null bytes that we have to remove.
|
||||
abs_cmdline = os.path.abspath(cmdline).replace("\x00", "")
|
||||
if cmdline.startswith(wine_exe_path) or abs_cmdline == str(self.game_executable):
|
||||
if cmdline.startswith(wine_exe_path) or abs_cmdline == str(
|
||||
self.game_executable
|
||||
):
|
||||
self._pid = dir
|
||||
break
|
||||
except:
|
||||
|
@ -9,6 +9,7 @@ from lmm.runners.base import runner_from_config
|
||||
|
||||
GAME_NAME = "ProjectDivaMegaMix"
|
||||
|
||||
|
||||
class ProjectDivaMegaMixProfile(Profile):
|
||||
# The names of the directories the the mods directory.
|
||||
mods: list[str]
|
||||
@ -21,19 +22,25 @@ class ProjectDivaMegaMixProfile(Profile):
|
||||
def get_mod_names(self) -> list[str]:
|
||||
return self.mods
|
||||
|
||||
|
||||
class ProjectDivaMegaMixGame(ProtonGame):
|
||||
def __init__(self, profiles: list[ProjectDivaMegaMixProfile], **kwargs):
|
||||
super().__init__(GAME_NAME, "1761390", profiles, **kwargs)
|
||||
|
||||
@property
|
||||
def installation_path(self) -> Path:
|
||||
return self.steam_library / "steamapps/common/Hatsune Miku Project DIVA Mega Mix Plus"
|
||||
return (
|
||||
self.steam_library
|
||||
/ "steamapps/common/Hatsune Miku Project DIVA Mega Mix Plus"
|
||||
)
|
||||
|
||||
@property
|
||||
def game_executable(self) -> Path:
|
||||
return self.installation_path / "DivaMegaMix.exe"
|
||||
|
||||
def prepare_overlays(self, profile: ProjectDivaMegaMixProfile) -> list[OverlayFSMount]:
|
||||
def prepare_overlays(
|
||||
self, profile: ProjectDivaMegaMixProfile
|
||||
) -> list[OverlayFSMount]:
|
||||
return [
|
||||
OverlayFSMount(
|
||||
upper=self.installation_path,
|
||||
@ -51,9 +58,11 @@ class ProjectDivaMegaMixGame(ProtonGame):
|
||||
"mods": [
|
||||
{
|
||||
"name": mod,
|
||||
} for mod in profile.mods
|
||||
]
|
||||
} for profile in self.profiles
|
||||
}
|
||||
for mod in profile.mods
|
||||
],
|
||||
}
|
||||
for profile in self.profiles
|
||||
]
|
||||
}
|
||||
|
||||
@ -61,9 +70,7 @@ class ProjectDivaMegaMixGame(ProtonGame):
|
||||
def from_dict(cls, data: dict[str, Any]) -> "ProjectDivaMegaMixGame":
|
||||
profiles = []
|
||||
for profile in data["profiles"]:
|
||||
mods = [
|
||||
mod["name"] for mod in profile["mods"]
|
||||
]
|
||||
mods = [mod["name"] for mod in profile["mods"]]
|
||||
|
||||
if "runner" in profile:
|
||||
runner = runner_from_config(profile["runner"])
|
||||
|
@ -7,6 +7,7 @@ from lmm.games.game import ProtonGame
|
||||
|
||||
GAME_NAME = "ReadyOrNot"
|
||||
|
||||
|
||||
class ReadyOrNotProfile(Profile):
|
||||
# Names of directories inside the game's mods directory.
|
||||
mods: list[str]
|
||||
@ -18,6 +19,7 @@ class ReadyOrNotProfile(Profile):
|
||||
def get_mod_names(self) -> list[str]:
|
||||
return self.mods
|
||||
|
||||
|
||||
class ReadyOrNotGame(ProtonGame):
|
||||
def __init__(self, profiles: list[ReadyOrNotProfile], **kwargs):
|
||||
super().__init__(GAME_NAME, "1144200", profiles, **kwargs)
|
||||
@ -55,9 +57,11 @@ class ReadyOrNotGame(ProtonGame):
|
||||
"mods": [
|
||||
{
|
||||
"name": mod,
|
||||
} for mod in profile.mods
|
||||
]
|
||||
} for profile in self.profiles
|
||||
}
|
||||
for mod in profile.mods
|
||||
],
|
||||
}
|
||||
for profile in self.profiles
|
||||
]
|
||||
}
|
||||
|
||||
@ -65,9 +69,7 @@ class ReadyOrNotGame(ProtonGame):
|
||||
def from_dict(cls, data: dict[str, Any]) -> "ReadyOrNotGame":
|
||||
profiles = []
|
||||
for profile in data["profiles"]:
|
||||
mods = [
|
||||
mod["name"] for mod in profile["mods"]
|
||||
]
|
||||
mods = [mod["name"] for mod in profile["mods"]]
|
||||
|
||||
if "runner" in profile:
|
||||
runner = runner_from_config(profile["runner"])
|
||||
|
@ -5,6 +5,7 @@ import zipfile
|
||||
from lmm.const import LMM_GAMES_PATH
|
||||
from lmm.games.bg3 import GAME_NAME
|
||||
|
||||
|
||||
def install_mod(mod: Path):
|
||||
match mod.suffix:
|
||||
case ".zip":
|
||||
@ -12,6 +13,7 @@ def install_mod(mod: Path):
|
||||
case _:
|
||||
print("Unknown mod")
|
||||
|
||||
|
||||
def install_zip_mod(mod: Path):
|
||||
# Inspect the archive to see if it's a PAK mod
|
||||
with zipfile.ZipFile(mod, "r") as f:
|
||||
|
@ -4,6 +4,7 @@ from typing import Optional
|
||||
|
||||
from lmm.cmd import run_sudo_cmd
|
||||
|
||||
|
||||
def compute_workdirs_location(mount: Path) -> Path:
|
||||
with open("/proc/mounts", "r") as f:
|
||||
mounts = f.read().split("\n")
|
||||
@ -35,7 +36,13 @@ class OverlayFSMount:
|
||||
|
||||
cd: Optional[Path]
|
||||
|
||||
def __init__(self, upper: Optional[Path], lower: list[Path | str], mount: Path, cd: Optional[Path] = None):
|
||||
def __init__(
|
||||
self,
|
||||
upper: Optional[Path],
|
||||
lower: list[Path | str],
|
||||
mount: Path,
|
||||
cd: Optional[Path] = None,
|
||||
):
|
||||
self.upper = upper
|
||||
self.lower = lower
|
||||
self.mount_path = mount
|
||||
@ -58,9 +65,7 @@ class OverlayFSMount:
|
||||
return Path(self.workdir.name)
|
||||
|
||||
def mount(self) -> Optional[Path]:
|
||||
lower = ":".join([
|
||||
str(l) for l in self.lower
|
||||
])
|
||||
lower = ":".join([str(l) for l in self.lower])
|
||||
options = f"lowerdir={lower}"
|
||||
if self.upper is not None:
|
||||
workdir = self.create_workdir()
|
||||
@ -69,8 +74,11 @@ class OverlayFSMount:
|
||||
result = run_sudo_cmd(
|
||||
[
|
||||
"mount",
|
||||
"-t", "overlay", "overlay",
|
||||
"-o", options,
|
||||
"-t",
|
||||
"overlay",
|
||||
"overlay",
|
||||
"-o",
|
||||
options,
|
||||
str(self.mount_path),
|
||||
],
|
||||
cd=self.cd,
|
||||
@ -87,10 +95,12 @@ class OverlayFSMount:
|
||||
print("Unmounting...")
|
||||
|
||||
# Remove the mount
|
||||
run_sudo_cmd([
|
||||
run_sudo_cmd(
|
||||
[
|
||||
"umount",
|
||||
str(self.mount_path),
|
||||
])
|
||||
]
|
||||
)
|
||||
|
||||
# Remove the temporary workdir
|
||||
if self.workdir is not None:
|
||||
|
@ -3,6 +3,7 @@ from typing import Optional
|
||||
|
||||
from lmm.const import LMM_GAMES_PATH
|
||||
|
||||
|
||||
class Profile:
|
||||
# The name of the game.
|
||||
game: str
|
||||
|
@ -7,6 +7,7 @@ from lmm.games.game import Game, ProtonGame
|
||||
from lmm.runners.runner import Runner
|
||||
from lmm.runners.steam import SteamFlatpakAppIdRunner
|
||||
|
||||
|
||||
def runner_from_config(data: dict[str, Any]) -> Optional[Runner]:
|
||||
match data["type"]:
|
||||
case "steam.flatpak":
|
||||
|
@ -2,6 +2,7 @@ import abc
|
||||
|
||||
from lmm.games.game import Game
|
||||
|
||||
|
||||
class Runner(abc.ABC):
|
||||
# Type identifier of the runner.
|
||||
runner_type: str
|
||||
|
@ -4,10 +4,12 @@ from pathlib import Path
|
||||
from lmm.runners.runner import Runner
|
||||
from lmm.games.game import Game
|
||||
|
||||
|
||||
class SteamRuntime(abc.ABC):
|
||||
def get_run_script(self) -> Path:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SteamSniperRuntime(SteamRuntime):
|
||||
library: Path
|
||||
|
||||
@ -15,7 +17,13 @@ class SteamSniperRuntime(SteamRuntime):
|
||||
self.library = library
|
||||
|
||||
def get_run_script(self) -> Path:
|
||||
return self.library / "steamapps" / "common" / "SteamLinuxRuntime_sniper" / "run-in-sniper"
|
||||
return (
|
||||
self.library
|
||||
/ "steamapps"
|
||||
/ "common"
|
||||
/ "SteamLinuxRuntime_sniper"
|
||||
/ "run-in-sniper"
|
||||
)
|
||||
|
||||
|
||||
class SteamSoldierRuntime(SteamRuntime):
|
||||
@ -25,7 +33,14 @@ class SteamSoldierRuntime(SteamRuntime):
|
||||
self.library = library
|
||||
|
||||
def get_run_script(self) -> Path:
|
||||
return self.library / "steamapps" / "common" / "SteamLinuxRuntime_soldier" / "run-in-soldier"
|
||||
return (
|
||||
self.library
|
||||
/ "steamapps"
|
||||
/ "common"
|
||||
/ "SteamLinuxRuntime_soldier"
|
||||
/ "run-in-soldier"
|
||||
)
|
||||
|
||||
|
||||
class SteamFlatpakAppIdRunner(Runner):
|
||||
def __init__(self):
|
||||
@ -34,12 +49,15 @@ class SteamFlatpakAppIdRunner(Runner):
|
||||
def run(self, game: Game):
|
||||
assert isinstance(game, ProtonGame)
|
||||
|
||||
run_cmd_nonblocking([
|
||||
run_cmd_nonblocking(
|
||||
[
|
||||
"flatpak",
|
||||
"run",
|
||||
"com.valvesoftware.Steam",
|
||||
f"steam://launch/{game.appid}/",
|
||||
])
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SteamFlatpakProtonRunner(Runner):
|
||||
# Path to the Proton prefix
|
||||
@ -55,7 +73,14 @@ class SteamFlatpakProtonRunner(Runner):
|
||||
# "waitforexitandrun" | "run"
|
||||
launchmode: str
|
||||
|
||||
def __init__(self, compat_data: Path, runtime: SteamRuntime, proton_version: str, game: str, launchmode: str = "waitforexitandrun"):
|
||||
def __init__(
|
||||
self,
|
||||
compat_data: Path,
|
||||
runtime: SteamRuntime,
|
||||
proton_version: str,
|
||||
game: str,
|
||||
launchmode: str = "waitforexitandrun",
|
||||
):
|
||||
self.compat_data = compat_data
|
||||
self.runtime = runtime
|
||||
self.game = game
|
||||
@ -81,7 +106,13 @@ class SteamFlatpakProtonRunner(Runner):
|
||||
|
||||
@property
|
||||
def _proton_path(self) -> Path:
|
||||
return Path.home() / "data" / "Steam" / "compatibilitytools.d" / self.proton_version
|
||||
return (
|
||||
Path.home()
|
||||
/ "data"
|
||||
/ "Steam"
|
||||
/ "compatibilitytools.d"
|
||||
/ self.proton_version
|
||||
)
|
||||
|
||||
def _build_launch_command(self) -> str:
|
||||
command = [
|
||||
@ -94,7 +125,9 @@ class SteamFlatpakProtonRunner(Runner):
|
||||
return commandline
|
||||
|
||||
def run(self):
|
||||
run_cmd_shell(" ".join([
|
||||
run_cmd_shell(
|
||||
" ".join(
|
||||
[
|
||||
"flatpak",
|
||||
"run",
|
||||
"--command=bash",
|
||||
@ -102,4 +135,6 @@ class SteamFlatpakProtonRunner(Runner):
|
||||
"com.valvesoftware.Steam",
|
||||
"-c",
|
||||
f'"{self._build_launch_command()}"',
|
||||
]))
|
||||
]
|
||||
)
|
||||
)
|
||||
|
@ -7,6 +7,7 @@ STEAM_PATHS = [
|
||||
Path.home() / ".var/app/com.valvesoftware.Steam/data/Steam",
|
||||
]
|
||||
|
||||
|
||||
def find_library_folder_for_game(appid: str) -> Optional[Path]:
|
||||
for steam_path in STEAM_PATHS:
|
||||
if not steam_path.exists():
|
||||
|
@ -7,5 +7,11 @@ dependencies = [
|
||||
"pyyaml"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"black",
|
||||
"pylint"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
lmm = "lmm.cli:main"
|
Loading…
Reference in New Issue
Block a user