diff --git a/docs/powertrain_build.md b/docs/powertrain_build.md
index 3b62df4..9d9b464 100644
--- a/docs/powertrain_build.md
+++ b/docs/powertrain_build.md
@@ -34,6 +34,35 @@ In git bash:
 py -3.6 -m powertrain_build.wrapper --codegen --models Models/ICEAES/VcAesTx/VcAesTx.mdl
 ```
 
+#### CLI Entrypoint
+
+If the Python `bin`/`Scripts` folder has been added to the `PATH`, you can also use
+`powertrain-build`'s CLI entrypoint in a similar way:
+
+```bash
+powertrain-build wrapper --codegen --models Models/ICEAES/VcAesTx/VcAesTx.mdl
+```
+
+In general, the python call
+
+```bash
+python -m powertrain_build.submodule.subsubmodule <args>
+```
+
+corresponds to the CLI entrypoint call
+
+```bash
+powertrain-build submodule subsubmodule <args>
+```
+
+Run
+
+```bash
+powertrain-build --help
+```
+
+for more information on how to use the CLI entrypoint.
+
 #### Set Matlab 2017 as Environmental Variable
 
 Add New User Variables
diff --git a/powertrain_build/__main__.py b/powertrain_build/__main__.py
new file mode 100644
index 0000000..7e04402
--- /dev/null
+++ b/powertrain_build/__main__.py
@@ -0,0 +1,6 @@
+import sys
+
+from powertrain_build.cli import main
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))
diff --git a/powertrain_build/check_interface.py b/powertrain_build/check_interface.py
index af5670d..fda9da8 100644
--- a/powertrain_build/check_interface.py
+++ b/powertrain_build/check_interface.py
@@ -12,6 +12,8 @@ import re
 import sys
 from itertools import product
 from pathlib import Path
+from textwrap import dedent
+from typing import List, Optional
 
 import git
 
@@ -340,14 +342,7 @@ def check_signals(left_signals, right_signals, errors, left_path=None, right_pat
     return serious_mismatch
 
 
-def parse_args():
-    """Parse arguments
-
-    Returns:
-        Namespace: the parsed arguments
-    """
-    parser = argparse.ArgumentParser(
-        description="""
+PARSER_HELP = dedent(r"""
     Checks attributes and existence of signals
 
     Produced but not consumed signals are giving warnings
@@ -368,50 +363,58 @@ def parse_args():
     py -3.6 -m powertrain_build.check_interface projects <Projects> \
             --projects ProjectOne ProjectTwo ProjectThree
     Checks the interfaces of ProjectOne, ProjectTwo and ProjectThree in the folder Projects
-    """,
-        formatter_class=argparse.RawTextHelpFormatter,
+""").strip()
+
+
+def configure_parser(parser: argparse.ArgumentParser):
+    """Configure arguments in parser."""
+    subparsers = parser.add_subparsers(
+        help="help for subcommand",
+        dest="mode",
+        required=True,
     )
-    subparsers = parser.add_subparsers(help="help for subcommand", dest="mode")
 
     # create the parser for the different commands
     model = subparsers.add_parser(
         "models",
-        description="""
-    Check models independently of projects.
+        description=dedent("""
+            Check models independently of projects.
 
-    All signals are assumed to be active.
-    Any signal that gives and error is used in a model but is not produced in any model or project
-    interface.
-    """,
+            All signals are assumed to be active.
+            Any signal that gives and error is used in a model but is not produced in any model or project
+            interface.
+        """).strip(),
     )
     add_model_args(model)
+    model.set_defaults(func=model_check)
 
     project = subparsers.add_parser(
         "projects",
-        description="""
-    Check projects as a whole.
+        description=dedent("""
+            Check projects as a whole.
 
-    It checks all models intenally and the SPM vs the interface.
-    """,
+            It checks all models intenally and the SPM vs the interface.
+        """).strip(),
     )
     add_project_args(project)
+    project.set_defaults(func=projects_check)
 
     models_in_projects = subparsers.add_parser(
         "models_in_projects",
-        description="""
-    Check models specifically for projects.
+        description=dedent("""
+            Check models specifically for projects.
 
-    Codeswitches are used to determine if the signals are produced and consumed in each model.
-    """,
+            Codeswitches are used to determine if the signals are produced and consumed in each model.
+        """).strip(),
     )
     add_project_args(models_in_projects)
     add_model_args(models_in_projects)
     models_in_projects.add_argument("--properties", help="Check properties such as type", action="store_true")
     models_in_projects.add_argument("--existence", help="Check signal existence consistency", action="store_true")
-    return parser.parse_args()
+    models_in_projects.set_defaults(func=models_in_projects_check)
 
 
-def add_project_args(parser):
+def add_project_args(parser: argparse.ArgumentParser):
     """Add project arguments to subparser"""
     parser.add_argument("project_root", help="Path to start looking for projects", type=Path)
     parser.add_argument(
@@ -419,7 +422,7 @@ def add_project_args(parser):
     )
 
 
-def add_model_args(parser):
+def add_model_args(parser: argparse.ArgumentParser):
     """Add model arguments to subparser"""
     parser.add_argument("model_root", help="Path to start looking for models", type=Path)
     parser.add_argument("--models", help="Name of models to check", nargs="+")
@@ -446,7 +449,7 @@ def model_path_to_name(model_paths):
     return model_names
 
 
-def model_check(args):
+def model_check(args: argparse.Namespace):
     """Entry point for models command."""
     serious_mismatch = False
     all_models = get_all_models(args.model_root)
@@ -460,10 +463,14 @@ def model_check(args):
         model_names = [model.name for model in all_models]
 
     serious_mismatch |= check_models_generic(all_models, model_names, [])
+
+    if serious_mismatch:
+        LOGGER.error("Serious interface errors found.")
+
     return serious_mismatch
 
 
-def projects_check(args):
+def projects_check(args: argparse.Namespace):
     """Entry point for projects command."""
     serious_mismatch = False
     projects = get_projects(args.project_root, args.projects)
@@ -472,10 +479,14 @@ def projects_check(args):
         serious_mismatch |= check_internal_signals(app, None)
         if ems is not None:
             serious_mismatch |= check_external_signals(ems, app, None)
+
+    if serious_mismatch:
+        LOGGER.error("Serious interface errors found.")
+
     return serious_mismatch
 
 
-def models_in_projects_check(args):
+def models_in_projects_check(args: argparse.Namespace):
     """Entry point for models_in_projects command."""
     serious_mismatch = False
     projects = get_projects(args.project_root, args.projects)
@@ -489,6 +500,10 @@ def models_in_projects_check(args):
         all_models = get_all_models(args.model_root)
         model_names = [model.name for model in all_models] if args.models is None else args.models
         serious_mismatch |= signal_existence(projects, model_names)
+
+    if serious_mismatch:
+        LOGGER.error("Serious interface errors found.")
+
     return serious_mismatch
 
 
@@ -545,19 +560,16 @@ def signal_match(signals_to_check, signals_to_check_against, matches):
                 matches[a_signal.name] = True
 
 
-def main():
+def main(argv: Optional[List[str]] = None):
     """Main function for stand alone execution."""
-    args = parse_args()
-    if args.mode == "models":
-        serious_errors = model_check(args)
-    if args.mode == "projects":
-        serious_errors = projects_check(args)
-    if args.mode == "models_in_projects":
-        serious_errors = models_in_projects_check(args)
-    if serious_errors:
-        LOGGER.error("Serious interface errors found.")
-        sys.exit(1)
+    parser = argparse.ArgumentParser(
+        description=PARSER_HELP,
+        formatter_class=argparse.RawTextHelpFormatter,
+    )
+    configure_parser(parser)
+    args = parser.parse_args(argv)
+    args.func(args)
 
 
 if __name__ == "__main__":
-    main()
+    main(sys.argv[1:])
diff --git a/powertrain_build/cli.py b/powertrain_build/cli.py
new file mode 100644
index 0000000..c82af9b
--- /dev/null
+++ b/powertrain_build/cli.py
@@ -0,0 +1,141 @@
+# Copyright 2024 Volvo Car Corporation
+# Licensed under Apache 2.0.
+
+from argparse import ArgumentParser, Namespace
+from typing import List, Optional
+
+import powertrain_build.check_interface
+import powertrain_build.create_conversion_table
+import powertrain_build.interface.export_global_vars
+import powertrain_build.interface.generate_adapters
+import powertrain_build.interface.generate_hi_interface
+import powertrain_build.interface.generate_service
+import powertrain_build.interface.generate_wrappers
+import powertrain_build.interface.model_yaml_verification
+import powertrain_build.interface.update_model_yaml
+import powertrain_build.interface.update_call_sources
+import powertrain_build.replace_compu_tab_ref
+import powertrain_build.signal_inconsistency_check
+from powertrain_build import __version__
+from powertrain_build.config import ProcessHandler
+from powertrain_build.lib import logger
+from powertrain_build.wrapper import PyBuildWrapper
+
+LOGGER = logger.create_logger(__file__)
+
+
+def parse_args(argv: Optional[List[str]] = None) -> Namespace:
+    """Parse command line arguments."""
+    parser = ArgumentParser(
+        prog="powertrain-build",
+        description="Powertrain-build",
+    )
+    parser.add_argument(
+        "-V", "--version",
+        action="version",
+        version=f"%(prog)s {__version__}",
+    )
+
+    command_subparsers = parser.add_subparsers(title="Commands", dest="command", required=True)
+
+    wrapper_parser = command_subparsers.add_parser(
+        "wrapper",
+        help=PyBuildWrapper.PARSER_HELP,
+    )
+    PyBuildWrapper.add_args(wrapper_parser)
+
+    config_parser = command_subparsers.add_parser(
+        "config",
+        help=ProcessHandler.PARSER_HELP,
+    )
+    ProcessHandler.configure_parser(config_parser)
+
+    check_interface_parser = command_subparsers.add_parser(
+        "check-interface",
+        help=powertrain_build.check_interface.PARSER_HELP,
+    )
+    powertrain_build.check_interface.configure_parser(check_interface_parser)
+
+    create_conversion_table_parser = command_subparsers.add_parser(
+        "create-conversion-table",
+        help=powertrain_build.create_conversion_table.PARSER_HELP,
+    )
+    powertrain_build.create_conversion_table.configure_parser(create_conversion_table_parser)
+
+    replace_compu_tab_ref_parser = command_subparsers.add_parser(
+        "replace-compu-tab-ref",
+        help=powertrain_build.replace_compu_tab_ref.PARSER_HELP,
+    )
+    powertrain_build.replace_compu_tab_ref.configure_parser(replace_compu_tab_ref_parser)
+
+    signal_inconsistency_check_parser = command_subparsers.add_parser(
+        "signal-inconsistency-check",
+        help=powertrain_build.signal_inconsistency_check.PARSER_HELP,
+    )
+    powertrain_build.signal_inconsistency_check.configure_parser(signal_inconsistency_check_parser)
+
+    interface_parser = command_subparsers.add_parser(
+        "interface",
+        help="Interface commands",
+    )
+    interface_subparsers = interface_parser.add_subparsers(
+        title="Interface commands",
+        dest="interface_command",
+        required=True,
+    )
+
+    export_global_vars_parser = interface_subparsers.add_parser(
+        "export-global-vars",
+        help=powertrain_build.interface.export_global_vars.PARSER_HELP,
+    )
+    powertrain_build.interface.export_global_vars.configure_parser(export_global_vars_parser)
+
+    generate_adapters_parser = interface_subparsers.add_parser(
+        "generate-adapters",
+        help=powertrain_build.interface.generate_adapters.PARSER_HELP,
+    )
+    powertrain_build.interface.generate_adapters.configure_parser(generate_adapters_parser)
+
+    generate_hi_interface_parser = interface_subparsers.add_parser(
+        "generate-hi-interface",
+        help=powertrain_build.interface.generate_hi_interface.PARSER_HELP,
+    )
+    powertrain_build.interface.generate_hi_interface.configure_parser(generate_hi_interface_parser)
+
+    generate_service_parser = interface_subparsers.add_parser(
+        "generate-service",
+        help=powertrain_build.interface.generate_service.PARSER_HELP,
+    )
+    powertrain_build.interface.generate_service.configure_parser(generate_service_parser)
+
+    generate_wrappers_parser = interface_subparsers.add_parser(
+        "generate-wrappers",
+        help=powertrain_build.interface.generate_wrappers.PARSER_HELP,
+    )
+    powertrain_build.interface.generate_wrappers.configure_parser(generate_wrappers_parser)
+
+    model_yaml_verification_parser = interface_subparsers.add_parser(
+        "model-yaml-verification",
+        help=powertrain_build.interface.model_yaml_verification.PARSER_HELP,
+    )
+    powertrain_build.interface.model_yaml_verification.configure_parser(model_yaml_verification_parser)
+
+    update_model_yaml_parser = interface_subparsers.add_parser(
+        "update-model-yaml",
+        help=powertrain_build.interface.update_model_yaml.PARSER_HELP,
+    )
+    powertrain_build.interface.update_model_yaml.configure_parser(update_model_yaml_parser)
+
+    update_call_sources_parser = interface_subparsers.add_parser(
+        "update-call-sources",
+        help=powertrain_build.interface.update_call_sources.PARSER_HELP,
+    )
+    powertrain_build.interface.update_call_sources.configure_parser(update_call_sources_parser)
+
+    return parser.parse_args(argv)
+
+
+def main(argv: Optional[List[str]] = None) -> Namespace:
+    """Run main function."""
+    args = parse_args(argv)
+    return args.func(args)
diff --git a/powertrain_build/config.py b/powertrain_build/config.py
index 9ac44cd..dfcc439 100644
--- a/powertrain_build/config.py
+++ b/powertrain_build/config.py
@@ -10,7 +10,9 @@ import json
 import operator
 import os
 import re
+import sys
 from pprint import pformat
+from typing import List, Optional
 
 from powertrain_build.lib import logger
 
@@ -412,17 +414,25 @@ class HeaderConfigParser(ConfigParserCommon):
 class ProcessHandler:
     """Class to collect functions for the process."""
 
+    PARSER_HELP = "Parse configs.json and c-files, to update code switch configs"
+
     @staticmethod
-    def parse_args():
+    def configure_parser(parser: argparse.ArgumentParser):
         """Parse arguments."""
-        parser = argparse.ArgumentParser("Parse configs.json and c-files, to update code switch configes")
-        subparser = parser.add_subparsers(title='Operation mode', dest='mode',
-                                          help="Run chosen files on in a number of directories")
+        parser.description = "Parse configs.json and c-files, to update code switch configs"
+
+        subparser = parser.add_subparsers(
+            title='Operation mode',
+            dest='mode',
+            help="Run chosen files on in a number of directories",
+            required=True,
+        )
         dir_parser = subparser.add_parser(
             'models',
             help="Run for one or multiple models. Script finds files generated from the model(s).")
         dir_parser.add_argument('models', nargs='+',
                                 help="Space separated list of model directories")
+
         file_parser = subparser.add_parser('files',
                                            help="Choose specific files. Mainly for manually written configs.")
         file_parser.add_argument('c_file',
@@ -433,8 +443,8 @@ class ProcessHandler:
                                  help="Full path to tl_aux file. (Optional) ")
         file_parser.add_argument('--local_file',
                                  help="Full path to OPort file. (Optional) ")
-        args = parser.parse_args()
-        return args
+
+        parser.set_defaults(func=ProcessHandler.main)
 
     @staticmethod
     def get_files(model_path):
@@ -505,9 +515,8 @@ class ProcessHandler:
         return parser.get_config()
 
     @classmethod
-    def main(cls):
+    def main(cls, args: argparse.Namespace):
         """Run the main function of the script."""
-        args = cls.parse_args()
         if args.mode == 'files':
             LOGGER.info('Using manually supplied files %s', args)
             local_defs = cls.get_header_config(args.local_file, {})
@@ -522,5 +531,13 @@ class ProcessHandler:
                     cls.update_config_file(c_file, config_file, aux_defs)
 
 
+def main(argv: Optional[List[str]] = None):
+    """Run main function."""
+    parser = argparse.ArgumentParser(ProcessHandler.PARSER_HELP)
+    ProcessHandler.configure_parser(parser)
+    args = parser.parse_args(argv)
+    return args.func(args)
+
+
 if __name__ == "__main__":
-    ProcessHandler.main()
+    main(sys.argv[1:])
diff --git a/powertrain_build/create_conversion_table.py b/powertrain_build/create_conversion_table.py
index 5d87c7c..18dd624 100644
--- a/powertrain_build/create_conversion_table.py
+++ b/powertrain_build/create_conversion_table.py
@@ -5,7 +5,12 @@
 
 import argparse
 import json
+import sys
 from pathlib import Path
+from typing import List, Optional
+
+
+PARSER_HELP = "Create a2l file from conversion_table.json file."
 
 
 def get_vtab_text(vtab):
@@ -40,22 +45,29 @@ def create_conversion_table(input_json: Path, output_a2l: Path):
             f_h.write(get_vtab_text(vtab))
 
 
-def parse_args():
-    """Parse args."""
-    parser = argparse.ArgumentParser('Create a2l file from conversion_table.json file')
+def create_conversion_table_cli(args: argparse.Namespace):
+    """CLI wrapper function for passing in Namespace object.
+
+    This allows maintaining a standardized CLI function signature while not breaking backwards
+    compatibility with create_converstion_table.
+    """
+    create_conversion_table(args.input_file, args.output_file)
+
+
+def configure_parser(parser: argparse.ArgumentParser):
+    """Set up parser for CLI."""
     parser.add_argument('input_file', type=Path)
     parser.add_argument('output_file', type=Path)
-    args = parser.parse_args()
-    return args
+    parser.set_defaults(func=create_conversion_table_cli)
 
 
-def main():
-    """Main."""
-    args = parse_args()
-    conversion_table_json = args.input_file
-    conversion_table_a2l = args.output_file
-    create_conversion_table(conversion_table_json, conversion_table_a2l)
+def main(argv: Optional[List[str]] = None):
+    """Main function for CLI."""
+    parser = argparse.ArgumentParser(description=PARSER_HELP)
+    configure_parser(parser)
+    args = parser.parse_args(argv)
+    args.func(args)
 
 
 if __name__ == '__main__':
-    main()
+    main(sys.argv[1:])
diff --git a/powertrain_build/interface/export_global_vars.py b/powertrain_build/interface/export_global_vars.py
index 6688886..074a873 100644
--- a/powertrain_build/interface/export_global_vars.py
+++ b/powertrain_build/interface/export_global_vars.py
@@ -6,7 +6,7 @@
 import argparse
 import os
 import sys
-from typing import Dict, Tuple
+from typing import Dict, List, Optional, Tuple
 
 from ruamel.yaml import YAML
 
@@ -15,6 +15,9 @@ from powertrain_build.feature_configs import FeatureConfigs
 from powertrain_build.unit_configs import UnitConfigs
 
 
+PARSER_HELP = "Export global variables."
+
+
 def get_global_variables(project_config_path: str) -> Dict:
     """Get global variables connected to PyBuild project.
 
@@ -81,18 +84,38 @@ def _export_yaml(data: Dict, file_path: str) -> None:
         yaml.dump(data, yaml_file)
 
 
-def _main():
-    args = _parse_args()
+def export_global_vars(args: argparse.Namespace):
+    """Exports global variables as yaml file."""
     global_variables = get_global_variables(args.project_config)
     _export_yaml(global_variables, args.output_file)
 
 
-def _parse_args():
-    parser = argparse.ArgumentParser(description="Export global variables.")
-    parser.add_argument("--project-config", help="Project root configuration file.", required=True)
-    parser.add_argument("--output-file", help="Output file to export global variables.", required=True)
-    return parser.parse_args()
+def _main(argv: Optional[List[str]] = None):
+    """Main function for CLI."""
+    parser = argparse.ArgumentParser(description=PARSER_HELP)
+    configure_parser(parser)
+    args = parser.parse_args(argv)
+    args.func(args)
+
+
+def configure_parser(parser: argparse.ArgumentParser):
+    """Configures the argument parser for the script.
+
+    Args:
+        parser (argparse.ArgumentParser): Argument parser.
+    """
+    parser.add_argument(
+        "--project-config",
+        help="Project root configuration file.",
+        required=True,
+    )
+    parser.add_argument(
+        "--output-file",
+        help="Output file to export global variables.",
+        required=True,
+    )
+    parser.set_defaults(func=export_global_vars)
 
 
 if __name__ == "__main__":
-    sys.exit(_main())
+    sys.exit(_main(sys.argv[1:]))
diff --git a/powertrain_build/interface/generate_adapters.py b/powertrain_build/interface/generate_adapters.py
index ab49b11..ee451b1 100644
--- a/powertrain_build/interface/generate_adapters.py
+++ b/powertrain_build/interface/generate_adapters.py
@@ -3,8 +3,12 @@
 
 # -*- coding: utf-8 -*-
 """Python module used for calculating interfaces for CSP"""
+import argparse
+import sys
 from pathlib import Path
 from os import path
+from typing import List, Optional
+
 from powertrain_build.interface.hal import HALA
 from powertrain_build.interface.device_proxy import DPAL
 from powertrain_build.interface.service import ServiceFramework
@@ -15,14 +19,11 @@ from powertrain_build.lib.helper_functions import deep_json_update
 
 LOGGER = logger.create_logger("CSP adapters")
 
+PARSER_HELP = "Generate adapters"
 
-def parse_args():
-    """Parse arguments
 
-    Returns:
-        Namespace: the parsed arguments
-    """
-    parser = generation_utils.base_parser()
+def configure_parser(parser: argparse.ArgumentParser):
+    generation_utils.add_base_args(parser)
     parser.add_argument(
         "--dp-interface",
         help="Add dp interface to adapter specification",
@@ -48,14 +49,20 @@ def parse_args():
         help="Update project config file with path to adapter specifications",
         action="store_true"
     )
-    return parser.parse_args()
+    parser.set_defaults(func=generate_adapters)
 
 
-def main():
+def main(argv: Optional[List[str]] = None):
     """ Main function for stand alone execution.
     Mostly useful for testing and generation of dummy hal specifications
     """
-    args = parse_args()
+    parser = argparse.ArgumentParser(description=PARSER_HELP)
+    configure_parser(parser)
+    args = parser.parse_args(argv)
+    args.func(args)
+
+
+def generate_adapters(args: argparse.Namespace):
     app = generation_utils.process_app(args.config)
     adapters(args, app)
 
@@ -122,4 +129,4 @@ def adapters(args, app):
 
 
 if __name__ == "__main__":
-    main()
+    main(sys.argv[1:])
diff --git a/powertrain_build/interface/generate_hi_interface.py b/powertrain_build/interface/generate_hi_interface.py
index 6e634d8..ecf578f 100644
--- a/powertrain_build/interface/generate_hi_interface.py
+++ b/powertrain_build/interface/generate_hi_interface.py
@@ -3,7 +3,11 @@
 
 # -*- coding: utf-8 -*-
 """Python module used for calculating interfaces for CSP HI"""
+import argparse
+import sys
 from pathlib import Path
+from typing import List, Optional
+
 from powertrain_build.interface import generation_utils
 from powertrain_build.interface.device_proxy import DPAL
 from powertrain_build.lib.helper_functions import recursive_default_dict, to_normal_dict
@@ -11,6 +15,8 @@ from powertrain_build.lib.helper_functions import recursive_default_dict, to_nor
 OP_READ = 'read'
 OP_WRITE = 'write'
 
+PARSER_HELP = "Generate HI YAML interface file."
+
 
 def generate_hi_interface(args, hi_interface):
     """Generate HI YAML interface file.
@@ -44,31 +50,38 @@ def generate_hi_interface(args, hi_interface):
     generation_utils.write_to_file(to_normal_dict(result), args.output, is_yaml=True)
 
 
-def parse_args():
-    """Parse arguments.
+def generate_hi_interface_cli(args: argparse.Namespace):
+    """CLI entrypoint for generating HI YAML interface file.
 
-    Returns:
-        Namespace: the parsed arguments.
+    Args:
+        args (Namespace): Arguments from command line.
     """
-    parser = generation_utils.base_parser()
-    parser.add_argument(
-        "output",
-        help="Output file with interface specifications.",
-        type=Path
-    )
-    return parser.parse_args()
-
-
-def main():
-    """ Main function for stand alone execution.
-    Mostly useful for testing and generation of dummy hal specifications.
-    """
-    args = parse_args()
     app = generation_utils.process_app(args.config)
     hi_app = DPAL(app)
     interface = generation_utils.get_interface(app, hi_app)
     generate_hi_interface(args, interface)
 
 
+def configure_parser(parser: argparse.ArgumentParser):
+    """Configure parser for generating HI YAML interface file."""
+    generation_utils.add_base_args(parser)
+    parser.add_argument(
+        "output",
+        help="Output file with interface specifications.",
+        type=Path
+    )
+    parser.set_defaults(func=generate_hi_interface_cli)
+
+
+def main(argv: Optional[List[str]] = None):
+    """ Main function for stand alone execution.
+    Mostly useful for testing and generation of dummy hal specifications.
+    """
+    parser = argparse.ArgumentParser(description=PARSER_HELP)
+    configure_parser(parser)
+    args = parser.parse_args(argv)
+    args.func(args)
+
+
 if __name__ == "__main__":
-    main()
+    main(sys.argv[1:])
diff --git a/powertrain_build/interface/generate_service.py b/powertrain_build/interface/generate_service.py
index db012a4..eb15bde 100644
--- a/powertrain_build/interface/generate_service.py
+++ b/powertrain_build/interface/generate_service.py
@@ -3,21 +3,23 @@
 
 # -*- coding: utf-8 -*-
 """Python module used for calculating interfaces for CSP"""
+import argparse
+import sys
 from pathlib import Path
+from typing import List, Optional
+
 from powertrain_build.interface.service import get_service
 from powertrain_build.lib import logger
 from powertrain_build.interface import generation_utils
 
 LOGGER = logger.create_logger("CSP service")
 
+PARSER_HELP = "Generate CSP service models"
 
-def parse_args():
-    """Parse command line arguments
 
-    Returns:
-        Namespace: Arguments from command line
-    """
-    parser = generation_utils.base_parser()
+def configure_parser(parser: argparse.ArgumentParser):
+    """Configure parser for CSP service generation"""
+    generation_utils.add_base_args(parser)
     parser.add_argument(
         "--client-name",
         help="Name of the context object in CSP. Defaults to project name."
@@ -27,19 +29,26 @@ def parse_args():
         help="Output directory for service models",
         type=Path
     )
-    return parser.parse_args()
+    parser.set_defaults(func=generate_service_cli)
 
 
-def main():
-    """ Main function for stand alone execution.
-    Mostly useful for testing and generation of dummy hal specifications
-    """
-    args = parse_args()
+def generate_service_cli(args: argparse.Namespace):
+    """CLI function for CSP service generation"""
     app = generation_utils.process_app(args.config)
     client_name = generation_utils.get_client_name(args, app)
     service(args, app, client_name)
 
 
+def main(argv: Optional[List[str]] = None):
+    """ Main function for stand alone execution.
+    Mostly useful for testing and generation of dummy hal specifications
+    """
+    parser = argparse.ArgumentParser(description=PARSER_HELP)
+    configure_parser(parser)
+    args = parser.parse_args(argv)
+    args.func(args)
+
+
 def service(args, app, client_name):
     """ Generate specifications for pt-scheduler wrappers.
 
@@ -57,4 +66,4 @@ def service(args, app, client_name):
 
 
 if __name__ == "__main__":
-    main()
+    main(sys.argv[1:])
diff --git a/powertrain_build/interface/generate_wrappers.py b/powertrain_build/interface/generate_wrappers.py
index 899a077..d93d458 100644
--- a/powertrain_build/interface/generate_wrappers.py
+++ b/powertrain_build/interface/generate_wrappers.py
@@ -3,7 +3,11 @@
 
 # -*- coding: utf-8 -*-
 """Python module used for calculating interfaces for CSP"""
+import argparse
+import sys
 from pathlib import Path
+from typing import List, Optional
+
 from powertrain_build.interface.hal import HALA, get_hal_list
 from powertrain_build.interface.device_proxy import DPAL
 from powertrain_build.interface.service import ServiceFramework, get_service_list
@@ -12,6 +16,8 @@ from powertrain_build.interface import generation_utils
 
 LOGGER = logger.create_logger("CSP wrappers")
 
+PARSER_HELP = "Generate specifications for pt-scheduler wrappers"
+
 
 def get_manifest(app, domain, client_name):
     """Get signal manifest for application
@@ -40,13 +46,9 @@ def get_manifest(app, domain, client_name):
     return dpal.to_manifest(client_name)
 
 
-def parse_args():
-    """Parse command line arguments
-
-    Returns:
-        Namespace: Arguments from command line
-    """
-    parser = generation_utils.base_parser()
+def configure_parser(parser: argparse.ArgumentParser):
+    """Configure parser for generating pt-scheduler wrappers."""
+    generation_utils.add_base_args(parser)
     parser.add_argument(
         "--client-name",
         help="Name of the context object in CSP. Defaults to project name."
@@ -71,19 +73,30 @@ def parse_args():
         help="Output file with service interface specifications",
         type=Path
     )
-    return parser.parse_args()
+    parser.set_defaults(func=generate_wrappers_cli)
 
 
-def main():
-    """ Main function for stand alone execution.
-    Mostly useful for testing and generation of dummy hal specifications
+def generate_wrappers_cli(args: argparse.Namespace):
+    """Generate specifications for pt-scheduler wrappers.
+
+    Args:
+        args (Namespace): Arguments from command line
     """
-    args = parse_args()
     app = generation_utils.process_app(args.config)
     client_name = generation_utils.get_client_name(args, app)
     wrappers(args, app, client_name)
 
 
+def main(argv: Optional[List[str]] = None):
+    """ Main function for stand alone execution.
+    Mostly useful for testing and generation of dummy hal specifications
+    """
+    parser = argparse.ArgumentParser(description=PARSER_HELP)
+    configure_parser(parser)
+    args = parser.parse_args(argv)
+    args.func(args)
+
+
 def wrappers(args, app, client_name):
     """ Generate specifications for pt-scheduler wrappers.
 
@@ -131,4 +144,4 @@ def wrappers(args, app, client_name):
 
 
 if __name__ == "__main__":
-    main()
+    main(sys.argv[1:])
diff --git a/powertrain_build/interface/generation_utils.py b/powertrain_build/interface/generation_utils.py
index c23064d..c5937c9 100644
--- a/powertrain_build/interface/generation_utils.py
+++ b/powertrain_build/interface/generation_utils.py
@@ -13,15 +13,13 @@ from powertrain_build.lib import logger
 LOGGER = logger.create_logger("CSP interface generation utils")
 
 
-def base_parser():
+def add_base_args(parser: argparse.ArgumentParser):
     """ Base parser that adds config argument.
 
     Returns:
         parser (ArgumentParser): Base parser
     """
-    parser = argparse.ArgumentParser()
     parser.add_argument("config", help="The project configuration file", type=Path)
-    return parser
 
 
 def get_client_name(args, app):
diff --git a/powertrain_build/interface/model_yaml_verification.py b/powertrain_build/interface/model_yaml_verification.py
index 8fdcccd..320cf97 100644
--- a/powertrain_build/interface/model_yaml_verification.py
+++ b/powertrain_build/interface/model_yaml_verification.py
@@ -5,12 +5,16 @@
 
 import argparse
 import logging
+import sys
+import typing
 from pathlib import Path
 from voluptuous import All, MultipleInvalid, Optional, Required, Schema
 from ruamel.yaml import YAML
 from powertrain_build.interface.application import Application
 from powertrain_build.interface.base import BaseApplication
 
+PARSER_HELP = "Verify the model yaml files."
+
 
 class ModelYmlVerification(BaseApplication):
     """Class for verifying the model yaml files."""
@@ -317,20 +321,14 @@ def get_app(project_config):
     return app
 
 
-def parse_args():
-    """Parse command line arguments.
-
-    Returns:
-        (Namespace): parsed command line arguments.
-    """
-    parser = argparse.ArgumentParser()
+def configure_parser(parser: argparse.ArgumentParser):
+    """Configure the argument parser."""
     parser.add_argument("config", help="The SPA2 project config file", type=Path)
-    return parser.parse_args()
+    parser.set_defaults(func=model_yaml_verification_cli)
 
 
-def main():
-    """Main function for model yaml verification."""
-    args = parse_args()
+def model_yaml_verification_cli(args: argparse.Namespace):
+    """CLI function for model yaml verification."""
     app = get_app(args.config)
     model_yamls = app.get_translation_files()
     model_yaml_ver = ModelYmlVerification(app)
@@ -338,5 +336,13 @@ def main():
     model_yaml_ver.print_success_msg()
 
 
+def main(argv: typing.Optional[typing.List[str]] = None):
+    """Main function for model yaml verification."""
+    parser = argparse.ArgumentParser(description=PARSER_HELP)
+    configure_parser(parser)
+    args = parser.parse_args(argv)
+    args.func(args)
+
+
 if __name__ == "__main__":
-    main()
+    main(sys.argv[1:])
diff --git a/powertrain_build/interface/update_call_sources.py b/powertrain_build/interface/update_call_sources.py
index 6c25722..dfa456a 100644
--- a/powertrain_build/interface/update_call_sources.py
+++ b/powertrain_build/interface/update_call_sources.py
@@ -6,17 +6,17 @@
 
 import argparse
 import re
+import sys
+from typing import List, Optional
 from ruamel.yaml import YAML
 from pathlib import Path
 
 
-def parse_args():
-    """ Parse arguments
+PARSER_HELP = "Update call sources for method calls in source files."
 
-    Returns:
-        Namespace: the parsed arguments
-    """
-    parser = argparse.ArgumentParser()
+
+def configure_parser(parser: argparse.ArgumentParser):
+    """Configure the parser for the update call sources command."""
     parser.add_argument("interface", help="Interface specification dict", type=Path)
     parser.add_argument("src_dir", help="Path to source file directory", type=Path)
     parser.add_argument(
@@ -26,12 +26,11 @@ def parse_args():
         default=None,
         help="Path to project config json file",
     )
-    return parser.parse_args()
+    parser.set_defaults(func=update_call_sources_cli)
 
 
-def main():
-    """ Main function for stand alone execution."""
-    args = parse_args()
+def update_call_sources_cli(args: argparse.Namespace):
+    """CLI function for updating call sources."""
     method_config = read_project_config(args.project_config)
     with open(args.interface, encoding="utf-8") as interface_file:
         yaml = YAML(typ='safe', pure=True)
@@ -39,6 +38,14 @@ def main():
     update_call_sources(args.src_dir, adapter_spec, method_config)
 
 
+def main(argv: Optional[List[str]] = None):
+    """ Main function for stand alone execution."""
+    parser = argparse.ArgumentParser(description=PARSER_HELP)
+    configure_parser(parser)
+    args = parser.parse_args(argv)
+    args.func(args)
+
+
 def read_project_config(project_config_path):
     """ Reads project config file and extract method specific settings if they are present.
 
@@ -170,4 +177,4 @@ def generate_src_code(adapter, method, old_src, method_config):
 
 
 if __name__ == "__main__":
-    main()
+    main(sys.argv[1:])
diff --git a/powertrain_build/interface/update_model_yaml.py b/powertrain_build/interface/update_model_yaml.py
index 4eb1211..016e7c0 100644
--- a/powertrain_build/interface/update_model_yaml.py
+++ b/powertrain_build/interface/update_model_yaml.py
@@ -5,11 +5,16 @@
 
 
 import argparse
+import sys
 from pathlib import Path
+from typing import List, Optional
+
 from ruamel.yaml import YAML
 from powertrain_build.interface.application import Application
 from powertrain_build.interface.base import BaseApplication
 
+PARSER_HELP = "Update model yaml files."
+
 
 class BadYamlFormat(Exception):
     """Exception to raise when signal is not in/out signal."""
@@ -155,25 +160,27 @@ def get_app(config):
     return app
 
 
-def parse_args():
-    """Parse command line arguments
-
-    Returns:
-        (Namespace): Parsed command line arguments
-    """
-    parser = argparse.ArgumentParser()
-    parser.add_argument("config", help="The SPA2 project config file", type=Path)
-    return parser.parse_args()
-
-
-def main():
-    """Main function for update model yaml."""
-    args = parse_args()
+def update_model_yaml_cli(args: argparse.Namespace):
+    """CLI for update model yaml."""
     app = get_app(args.config)
     translation_files = app.get_translation_files()
     uymlf = UpdateYmlFormat(app)
     uymlf.parse_definition(translation_files)
 
 
+def configure_parser(parser: argparse.ArgumentParser):
+    """Configure parser for update model yaml."""
+    parser.add_argument("config", help="The SPA2 project config file", type=Path)
+    parser.set_defaults(func=update_model_yaml_cli)
+
+
+def main(argv: Optional[List[str]] = None):
+    """Main function for update model yaml."""
+    parser = argparse.ArgumentParser(description=PARSER_HELP)
+    configure_parser(parser)
+    args = parser.parse_args(argv)
+    return args.func(args)
+
+
 if __name__ == "__main__":
-    main()
+    main(sys.argv[1:])
diff --git a/powertrain_build/replace_compu_tab_ref.py b/powertrain_build/replace_compu_tab_ref.py
index 6b41197..6e7a8b9 100644
--- a/powertrain_build/replace_compu_tab_ref.py
+++ b/powertrain_build/replace_compu_tab_ref.py
@@ -3,18 +3,29 @@
 
 """Module for replacing $CVC_* style references in a2l file."""
 
-import re
 import argparse
-
+import re
+import sys
 from pathlib import Path
+from typing import List, Optional
 
 
-def parse_args():
-    """Parse args."""
-    parser = argparse.ArgumentParser("Replace $CVC_* style references in a2l file")
+PARSER_HELP = "Replace $CVC_* style references in a2l file"
+
+
+def configure_parser(parser: argparse.ArgumentParser):
+    """Configure parser for CLI."""
     parser.add_argument("a2l_target_file")
-    args = parser.parse_args()
-    return args
+    parser.set_defaults(func=replace_tab_verb_cli)
+
+
+def replace_tab_verb_cli(args: argparse.Namespace):
+    """CLI wrapper function for passing in Namespace object.
+
+    This allows maintaining a standardized CLI function signature while not breaking backwards
+    compatibility with replace_tab_verb.
+    """
+    replace_tab_verb(args.a2l_target_file)
 
 
 def replace_tab_verb(file_path: Path):
@@ -82,11 +93,13 @@ def replace_tab_verb(file_path: Path):
         f_h.write(a2l_patched)
 
 
-def main():
-    """Main."""
-    args = parse_args()
-    replace_tab_verb(args.a2l_target_file)
+def main(argv: Optional[List[str]] = None):
+    """Main function for CLI."""
+    parser = argparse.ArgumentParser(description=PARSER_HELP)
+    configure_parser(parser)
+    args = parser.parse_args(argv)
+    args.func(args)
 
 
 if __name__ == "__main__":
-    main()
+    main(sys.argv[1:])
diff --git a/powertrain_build/signal_inconsistency_check.py b/powertrain_build/signal_inconsistency_check.py
index 343b038..1b25c59 100644
--- a/powertrain_build/signal_inconsistency_check.py
+++ b/powertrain_build/signal_inconsistency_check.py
@@ -11,6 +11,7 @@ import sys
 from collections import defaultdict
 from os.path import join
 from pathlib import Path
+from typing import List, Optional
 
 import git
 
@@ -60,6 +61,8 @@ TEMPLATE = """<!DOCTYPE html>
 </body>
 </html>"""
 
+PARSER_HELP = "Run signal inconsistency check."
+
 
 def gen_sig_incons_index_file(project_list):
     """Generate Index_SigCheck_All.html."""
@@ -74,9 +77,8 @@ def gen_sig_incons_index_file(project_list):
         f_h.write(TEMPLATE.format(project_rows=rows))
 
 
-def parse_args():
+def configure_parser(parser: argparse.ArgumentParser):
     """Parse the arguments sent to the script."""
-    parser = argparse.ArgumentParser("")
     parser.add_argument(
         "-m",
         "--models",
@@ -89,7 +91,7 @@ def parse_args():
     parser.add_argument(
         "-r", "--report", help="Create report for all projects", action="store_true"
     )
-    return parser.parse_args()
+    parser.set_defaults(func=run_signal_inconsistency_check)
 
 
 def get_project_configs():
@@ -612,12 +614,19 @@ class SignalInconsistency:
         return exit_code
 
 
-def main():
+def run_signal_inconsistency_check(args: argparse.Namespace) -> int:
     """Create Signal Inconsistency instance and run checks."""
-    args = parse_args()
     sig_in = SignalInconsistency(args)
     return sig_in.run(args)
 
 
+def main(argv: Optional[List[str]] = None) -> int:
+    """Create Signal Inconsistency instance and run checks."""
+    parser = argparse.ArgumentParser(description=PARSER_HELP)
+    configure_parser(parser)
+    args = parser.parse_args(argv)
+    return args.func(args)
+
+
 if __name__ == "__main__":
-    sys.exit(main())
+    sys.exit(main(sys.argv[1:]))
diff --git a/powertrain_build/wrapper.py b/powertrain_build/wrapper.py
index 5c2ced1..742bbcb 100644
--- a/powertrain_build/wrapper.py
+++ b/powertrain_build/wrapper.py
@@ -10,6 +10,7 @@ import os
 import sys
 from pathlib import Path
 from re import search
+from typing import List, Optional
 
 try:
     from importlib.resources import files
@@ -28,6 +29,7 @@ class PyBuildWrapper(pt_matlab.Matlab):
     """Performs upgrade of Matlab models to PyBuild system."""
 
     HASH_FILE_NAME = "pybuild_file_hashes.json"
+    PARSER_HELP = "Run PyBuild, update and/or generate code for selected models and/or build."
 
     def __init__(self, args):
         """Constructor, initializes paths for PyBuild upgrader.
@@ -469,8 +471,10 @@ class PyBuildWrapper(pt_matlab.Matlab):
             default="matlab-scripts",
             help="Path to folder containing Matlab scripts and simulink libraries to include.",
         )
-        powertrain_build_parser = parser.add_subparsers(help="PyBuild specific build.")
-        build_specific_parser = powertrain_build_parser.add_parser(
+        parser.set_defaults(func=run_wrapper)
+
+        subparsers = parser.add_subparsers(help="PyBuild specific build.")
+        build_specific_parser = subparsers.add_parser(
             "build-specific", help="Run PyBuild for project with specific settings."
         )
         build.add_args(build_specific_parser)
@@ -479,12 +483,8 @@ class PyBuildWrapper(pt_matlab.Matlab):
         pt_matlab.Matlab.add_args(parser)
 
 
-def main():
-    """Run main function."""
-    parser = argparse.ArgumentParser("PyBuild Wrapper")
-    PyBuildWrapper.add_args(parser)
-    args = parser.parse_args()
-
+def run_wrapper(args: argparse.Namespace) -> int:
+    """Run PyBuildWrapper."""
     if args.build is not None and getattr(args, "project_config", None) is not None:
         LOGGER.error("Cannot run both PyBuild quick build (--build <PROJECT>) " "and specific build (build-specific).")
         return 1
@@ -498,5 +498,13 @@ def main():
     return wrapper.run()
 
 
+def main(argv: Optional[List[str]] = None) -> int:
+    """Run PyBuildWrapper"""
+    parser = argparse.ArgumentParser("PyBuild Wrapper")
+    PyBuildWrapper.add_args(parser)
+    args = parser.parse_args(argv)
+    return run_wrapper(args)
+
+
 if __name__ == "__main__":
-    sys.exit(main())
+    sys.exit(main(sys.argv[1:]))
diff --git a/requirements.txt b/requirements.txt
index b2b6843..a62eca3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,3 +12,5 @@ scipy==1.5.4; python_version < "3.8"
 scipy==1.9.1; python_version == "3.8" or python_version == "3.9" or python_version == "3.10"
 scipy==1.14.1; python_version >= "3.11"
 importlib-resources==5.4.0; python_version < "3.9"
+pywin32==305; python_version == "3.6" and sys_platform == "win32"
+pywin32==308; python_version > "3.6" and sys_platform == "win32"
diff --git a/setup.cfg b/setup.cfg
index b73371b..d4a7a5c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -34,3 +34,7 @@ packages =
 
 [pbr]
 skip_git_sdist = 1
+
+[options.entry_points]
+console_scripts =
+    powertrain-build = powertrain_build.cli:main
diff --git a/tests/powertrain_build/func_cli.py b/tests/powertrain_build/func_cli.py
new file mode 100644
index 0000000..60c9406
--- /dev/null
+++ b/tests/powertrain_build/func_cli.py
@@ -0,0 +1,24 @@
+import sys
+from subprocess import run
+from unittest import TestCase
+
+
+class TestCli(TestCase):
+    """Test the cli module."""
+
+    def test_entry_points(self):
+        """Tests that entrypoints are correctly defined and work both as __main__ and sub-commands."""
+        modules = [
+            "powertrain_build.wrapper",
+            "powertrain_build.interface.generate_adapters",
+            "powertrain_build.interface.generate_hi_interface",
+            "powertrain_build.interface.generate_service",
+            "powertrain_build.interface.generate_wrappers",
+            "powertrain_build.interface.model_yaml_verification",
+            "powertrain_build.interface.update_model_yaml",
+            "powertrain_build.interface.update_call_sources",
+        ]
+        for module in modules:
+            entrypoint = module.replace("_", "-").split(".")
+            run(entrypoint + ["--help"], check=True)
+            run([sys.executable, "-m", module, "--help"], check=True)
diff --git a/tests/powertrain_build/test_cli.py b/tests/powertrain_build/test_cli.py
new file mode 100644
index 0000000..e139808
--- /dev/null
+++ b/tests/powertrain_build/test_cli.py
@@ -0,0 +1,46 @@
+"""Tests for the cli module."""
+
+from pathlib import Path
+from unittest import TestCase
+from unittest.mock import patch
+
+from powertrain_build.cli import main
+
+
+class TestCli(TestCase):
+    """Test the cli module."""
+
+    def test_main(self):
+        """Test that the main function parses arguments and calls correct function."""
+        # Simple command without arguments
+        with patch("powertrain_build.wrapper.run_wrapper") as run_wrapper_mock:
+            main(["wrapper"])
+            run_wrapper_mock.assert_called_once()
+            args = run_wrapper_mock.call_args[0][0]
+            self.assertEqual(args.func, run_wrapper_mock)
+            self.assertEqual(args.command, "wrapper")
+        # Sub-command with arguments
+        with patch("powertrain_build.interface.generate_adapters.generate_adapters") as generate_adapters_mock:
+            main([
+                "interface", "generate-adapters",
+                "--dp-interface",
+                "--hal-interface",
+                "--service-interface",
+                "--update-config",
+                "path/to/config",
+                "path/to/output",
+            ])
+            generate_adapters_mock.assert_called_once()
+            args = generate_adapters_mock.call_args[0][0]
+            self.assertEqual(args.func, generate_adapters_mock)
+            self.assertEqual(args.command, "interface")
+            self.assertEqual(args.interface_command, "generate-adapters")
+            self.assertTrue(args.dp_interface)
+            self.assertTrue(args.hal_interface)
+            self.assertTrue(args.service_interface)
+            self.assertEqual(args.config, Path("path/to/config"))
+            self.assertEqual(args.output, Path("path/to/output"))
+        # Command without enough arguments
+        with self.assertRaises(SystemExit) as system_exit:
+            main(["interface", "generate-adapters"])
+            self.assertEqual(system_exit.exception.code, 2)
diff --git a/tox.ini b/tox.ini
index 3006869..28ab6d2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,10 @@
 [tox]
-skipsdist = True
 requires =
     tox >= 2.0
+envlist =
+    flake8
+    pytest
+    functest
 
 [flake8]
 exclude =
@@ -42,3 +45,9 @@ exclude_lines =
 
 [coverage:html]
 directory = cover
+
+[testenv:functest]
+skipsdist = False
+package = editable
+commands =
+    pytest tests/powertrain_build/func_cli.py
diff --git a/zuul.d/tox.yaml b/zuul.d/tox.yaml
index 477e6fe..f6bd335 100644
--- a/zuul.d/tox.yaml
+++ b/zuul.d/tox.yaml
@@ -2,5 +2,5 @@
     name: powertrain-build-tox
     parent: tox-cover
     vars:
-      tox_envlist: flake8,pytest
+      tox_envlist: flake8,pytest,functest
     nodeset: ubuntu-jammy