blob: 8f3dc8c48a7dcf636946ea891bf4ae855d855f9e [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import os
import sys
from typing import Any
from cli.commands.base import BaseCommand
def resolve_path(filepath: str) -> str | None:
"""Resolve a file path to an absolute path.
Paths must be fully qualified from the workspace root (FUCHSIA_DIR), or
be an absolute path, or exist relative to the current working directory.
"""
abs_path = os.path.abspath(filepath)
if os.path.isfile(abs_path):
return abs_path
if os.path.isabs(filepath):
return None
fuchsia_dir = os.environ.get("FUCHSIA_DIR")
if not fuchsia_dir:
return None
fuchsia_path = os.path.abspath(os.path.join(fuchsia_dir, filepath))
if os.path.isfile(fuchsia_path):
return fuchsia_path
return None
class Command(BaseCommand):
COMMAND_NAME = "break"
@staticmethod
def register_cli(subparsers: Any) -> None:
break_parser = subparsers.add_parser(
Command.COMMAND_NAME,
aliases=[
"breakpoint",
"b",
"setBreakpoints",
"set-breakpoints",
"set_breakpoints",
],
help="Set or delete a breakpoint",
description=(
"Set or delete a breakpoint at the specified file and line. "
"Paths must be fully qualified from the workspace root, "
"an absolute path, or relative to the current working directory. "
"To delete a breakpoint, the file and line specification "
"must match an existing breakpoint. "
"To view currently installed breakpoints, use the get-state command."
),
)
break_parser.add_argument(
"file_line",
type=str,
help="File and line number (fully qualified from workspace root, absolute, or relative to CWD), e.g. 'main.cc:23' or 'src/foo.rs:10'",
)
break_parser.add_argument(
"-d",
"--delete",
action="store_true",
help=(
"Delete the breakpoint at the specified file and line "
"instead of adding it"
),
)
@staticmethod
def _parse_and_resolve_file_line(
file_line: str,
) -> tuple[str, int] | None:
"""Parses '<file>:<line>' and resolves the file path against the workspace."""
if ":" not in file_line:
print(
f"Error: Invalid format '{file_line}'. Expected '<file>:<line>'.",
file=sys.stderr,
)
return None
parts = file_line.rsplit(":", 1)
file_part = parts[0]
line_part = parts[1]
try:
line = int(line_part)
if line <= 0:
raise ValueError()
except ValueError:
print(
f"Error: Invalid line number '{line_part}'. Must be a positive integer.",
file=sys.stderr,
)
return None
resolved_file = resolve_path(file_part)
if not resolved_file:
print(
f"Error: Could not resolve file path '{file_part}'. "
"Paths must be fully qualified from the workspace root, "
"an absolute path, or relative to the current working directory. "
"Ensure FUCHSIA_DIR is set or run from the workspace root.",
file=sys.stderr,
)
return None
return resolved_file, line
@staticmethod
async def execute(args: argparse.Namespace) -> int | None:
# In JSON mode (--json), cli.py invokes execute() with a namespace converted from
# an already-deserialized BreakRequest (which has 'file' and 'line', but no 'file_line').
# In CLI mode, argparse populates 'file_line', which we must split and resolve.
if hasattr(args, "file") and hasattr(args, "line"):
return None
parsed = Command._parse_and_resolve_file_line(args.file_line)
if parsed is None:
return 1
args.file, args.line = parsed
# Remove CLI-only file_line attribute so the namespace matches the JSON BreakRequest structure (file and line).
if hasattr(args, "file_line"):
delattr(args, "file_line")
return None