Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Update to use pvi 0.8 #181

Merged
merged 4 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/developer/explanations/entities.rst
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually like this sub-folder approach more - it is clearer what is going on as this file is really always called st.cmd

Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Click the arrows to reveal the files.
<details>
<summary><a>st.cmd</a></summary>

.. include:: ../../../tests/samples/outputs/motorSim.st.cmd
.. include:: ../../../tests/samples/outputs/motorSim/st.cmd
:literal:

.. raw:: html
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ dependencies = [
"ruamel.yaml",
"jinja2",
"GitPython",
"pvi>=0.7.1",
"pvi~=0.8", # pvi currently tracks breaking changes with minor version
] # Add project dependencies here, e.g. ["click", "numpy"]
dynamic = ["version"]
license.file = "LICENSE"
Expand Down
4 changes: 4 additions & 0 deletions src/ibek/gen_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ def ioc_deserialize(ioc_instance_yaml: Path, definition_yaml: List[Path]) -> IOC

# extract the ioc instance yaml into a dict
ioc_instance_dict = YAML(typ="safe").load(ioc_instance_yaml)
if ioc_instance_dict is None or "ioc_name" not in ioc_instance_dict:
raise RuntimeError(
f"Failed to load a valid ioc config from {ioc_instance_yaml}"
)

# extract the ioc name into UTILS for use in jinja renders
name = UTILS.render({}, ioc_instance_dict["ioc_name"])
Expand Down
37 changes: 28 additions & 9 deletions src/ibek/globals.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be the new pattern for global config!

Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,36 @@
from pydantic import BaseModel, ConfigDict
from typer.core import TyperGroup


class _Globals:
"""Helper class for accessing global constants."""

def __init__(self) -> None:
self.EPICS_ROOT = Path(os.getenv("EPICS_ROOT", "/epics/"))
"""Root of epics directory tree.

This can be overriden by defining an environment variable "EPICS_ROOT".
"""

self.IBEK_DEFS = self.EPICS_ROOT / "ibek-defs"
"""Directory containing ibek support yaml definitions."""

self.PVI_DEFS = self.EPICS_ROOT / "pvi-defs"
"""Directory containing pvi device yaml definitions."""

self.RUNTIME_OUTPUT = self.EPICS_ROOT / "runtime"
"""Directory containing runtime generated assets for IOC boot."""

self.OPI_OUTPUT = self.EPICS_ROOT / "opi"
"""Directory containing runtime generated opis to serve over http."""


GLOBALS = _Globals()

# TODO: Include all constants in _Globals

# get the container paths from environment variables
EPICS_BASE = Path(os.getenv("EPICS_BASE", "/epics/epics-base"))
EPICS_ROOT = Path(os.getenv("EPICS_ROOT", "/epics/"))
IOC_FOLDER = Path(os.getenv("IOC", "/epics/ioc"))
SUPPORT = Path(os.getenv("SUPPORT", "/epics/support"))
CONFIG_DIR_NAME = "config"
Expand All @@ -26,19 +53,11 @@
# Folder containing templates for IOC src etc.
TEMPLATES = Path(__file__).parent / "templates"

# Definitions populated at container build time
IBEK_DEFS = EPICS_ROOT / "ibek-defs"
PVI_DEFS = EPICS_ROOT / "pvi-defs"

# Paths for ibek-support
IBEK_GLOBALS = Path("_global")
SUPPORT_YAML_PATTERN = "*ibek.support.yaml"
PVI_YAML_PATTERN = "*pvi.device.yaml"

# Assets generated at runtime
RUNTIME_OUTPUT_PATH = EPICS_ROOT / "runtime"
OPI_OUTPUT_PATH = EPICS_ROOT / "opi"

IOC_DBDS = SUPPORT / "configure/dbd_list"
IOC_LIBS = SUPPORT / "configure/lib_list"
RUNTIME_DEBS = SUPPORT / "configure/runtime_debs"
Expand Down
6 changes: 3 additions & 3 deletions src/ibek/ioc_cmds/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import typer

from ibek.globals import IBEK_DEFS, IOC_FOLDER, PVI_DEFS
from ibek.globals import GLOBALS, IOC_FOLDER

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -54,8 +54,8 @@ def extract_assets(
if defaults:
default_assets = [
source / "support" / "configure",
PVI_DEFS,
IBEK_DEFS,
GLOBALS.PVI_DEFS,
GLOBALS.IBEK_DEFS,
IOC_FOLDER,
Path("/venv"),
]
Expand Down
8 changes: 4 additions & 4 deletions src/ibek/ioc_cmds/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from ibek.gen_scripts import ioc_create_model
from ibek.globals import (
IBEK_DEFS,
GLOBALS,
SUPPORT_YAML_PATTERN,
NaturalOrderGroup,
)
Expand Down Expand Up @@ -57,7 +57,7 @@ def generate_schema(
),
] = None,
ibek_defs: bool = typer.Option(
True, help=f"Include definitions in {IBEK_DEFS} in generated schema"
True, help=f"Include definitions in {GLOBALS.IBEK_DEFS} in generated schema"
),
):
"""
Expand All @@ -71,10 +71,10 @@ def generate_schema(
if ibek_defs:
# this allows us to use the definitions inside the container
# which are in a known location after the container is built
definitions += IBEK_DEFS.glob(SUPPORT_YAML_PATTERN)
definitions += GLOBALS.IBEK_DEFS.glob(SUPPORT_YAML_PATTERN)

if not definitions:
log.error(f"No `definitions` given and none found in {IBEK_DEFS}")
log.error(f"No `definitions` given and none found in {GLOBALS.IBEK_DEFS}")
raise typer.Exit(1)

ioc_model = ioc_create_model(definitions)
Expand Down
10 changes: 2 additions & 8 deletions src/ibek/render_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,8 @@ def add_row(self, filename: str, args: Mapping[str, Any], entity: Entity) -> Non
columns=[0] * len(args),
)

# Add a new row of argument values.
# Note the case where no value was specified for the argument
# meaning that the name is to be rendered in Jinja
row = ["{{ %s }}" % k if v is None else v for k, v in args.items()]

# render any Jinja fields in the arguments
for i, line in enumerate(row):
row[i] = UTILS.render(dict(entity), row[i])
# add a new row of argument values, rendering any Jinja template fields
row = list(UTILS.render_map(dict(entity), args).values())

# save the new row
self.render_templates[filename].rows.append(row)
Expand Down
75 changes: 31 additions & 44 deletions src/ibek/runtime_cmds/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,13 @@
from pvi.device import Device

from ibek.gen_scripts import create_boot_script, create_db_script, ioc_deserialize
from ibek.globals import (
OPI_OUTPUT_PATH,
PVI_DEFS,
RUNTIME_OUTPUT_PATH,
NaturalOrderGroup,
)
from ibek.globals import GLOBALS, NaturalOrderGroup
from ibek.ioc import IOC, Entity
from ibek.support import Database
from ibek.utils import UTILS

runtime_cli = typer.Typer(cls=NaturalOrderGroup)

PVI_PV_PREFIX = "${prefix}"


@runtime_cli.command()
def generate(
Expand All @@ -36,16 +29,6 @@ def generate(
help="The filepath to a support module definition file",
autocompletion=lambda: [], # Forces path autocompletion
),
out: Path = typer.Option(
default=RUNTIME_OUTPUT_PATH / "st.cmd",
help="Path to output startup script",
autocompletion=lambda: [], # Forces path autocompletion
),
db_out: Path = typer.Option(
default=RUNTIME_OUTPUT_PATH / "ioc.subst",
help="Path to output database expansion shell script",
autocompletion=lambda: [], # Forces path autocompletion
),
):
"""
Build a startup script for an IOC instance
Expand All @@ -55,25 +38,24 @@ def generate(

ioc_instance = ioc_deserialize(instance, definitions)

# Clear out generated files so developers know if something stop being generated
shutil.rmtree(RUNTIME_OUTPUT_PATH, ignore_errors=True)
RUNTIME_OUTPUT_PATH.mkdir(exist_ok=True)
shutil.rmtree(OPI_OUTPUT_PATH, ignore_errors=True)
OPI_OUTPUT_PATH.mkdir(exist_ok=True)
# Clear out generated files so developers know if something stops being generated
shutil.rmtree(GLOBALS.RUNTIME_OUTPUT, ignore_errors=True)
GLOBALS.RUNTIME_OUTPUT.mkdir(exist_ok=True)
shutil.rmtree(GLOBALS.OPI_OUTPUT, ignore_errors=True)
GLOBALS.OPI_OUTPUT.mkdir(exist_ok=True)

pvi_index_entries, pvi_databases = generate_pvi(ioc_instance)
generate_index(ioc_instance.ioc_name, pvi_index_entries)

script_txt = create_boot_script(ioc_instance)

out.parent.mkdir(parents=True, exist_ok=True)

with out.open("w") as stream:
script_output = GLOBALS.RUNTIME_OUTPUT / "st.cmd"
script_output.parent.mkdir(parents=True, exist_ok=True)
with script_output.open("w") as stream:
stream.write(script_txt)

db_txt = create_db_script(ioc_instance, pvi_databases)

with db_out.open("w") as stream:
db_output = GLOBALS.RUNTIME_OUTPUT / "ioc.subst"
with db_output.open("w") as stream:
stream.write(db_txt)


Expand All @@ -99,37 +81,42 @@ def generate_pvi(ioc: IOC) -> Tuple[List[IndexEntry], List[Tuple[Database, Entit
if entity_pvi is None:
continue

pvi_yaml = PVI_DEFS / entity_pvi.yaml_path
pvi_yaml = GLOBALS.PVI_DEFS / entity_pvi.yaml_path
device_name = pvi_yaml.name.split(".")[0]
device_bob = OPI_OUTPUT_PATH / f"{device_name}.pvi.bob"
device_bob = GLOBALS.OPI_OUTPUT / f"{device_name}.pvi.bob"

# Skip deserializing yaml if not needed
if entity_pvi.pva_template or device_name not in formatted_pvi_devices:
if entity_pvi.pv or device_name not in formatted_pvi_devices:
device = Device.deserialize(pvi_yaml)
device.deserialize_parents([PVI_DEFS])
device.deserialize_parents([GLOBALS.PVI_DEFS])

# Render the prefix value for the device from the instance parameters
macros = {"prefix": UTILS.render(entity.model_dump(), entity_pvi.prefix)}

if entity_pvi.pva_template:
if entity_pvi.pv:
# Create a template with the V4 structure defining a PVI interface
output_template = RUNTIME_OUTPUT_PATH / f"{device_name}.pvi.template"
format_template(device, PVI_PV_PREFIX, output_template)
output_template = GLOBALS.RUNTIME_OUTPUT / f"{device_name}.pvi.template"
format_template(device, entity_pvi.pv_prefix, output_template)

# Add to extra databases to be added into substitution file
databases.append(
(Database(file=output_template.name, args=macros), entity)
(
Database(file=output_template.name, args=entity_pvi.ui_macros),
entity,
)
)

if device_name not in formatted_pvi_devices:
formatter.format(device, PVI_PV_PREFIX, device_bob)
formatter.format(device, device_bob)

# Don't format further instance of this device
formatted_pvi_devices.append(device_name)

if entity_pvi.index:
if entity_pvi.ui_index:
macros = UTILS.render_map(dict(entity), entity_pvi.ui_macros)
index_entries.append(
IndexEntry(label=device_name, ui=device_bob.name, macros=macros)
IndexEntry(
label=f"{device.label}",
ui=device_bob.name,
macros=macros,
)
)

return index_entries, databases
Expand All @@ -143,4 +130,4 @@ def generate_index(title: str, index_entries: List[IndexEntry]):
index_entries: List of entries to include as buttons on index UI

"""
DLSFormatter().format_index(title, index_entries, OPI_OUTPUT_PATH / "index.bob")
DLSFormatter().format_index(title, index_entries, GLOBALS.OPI_OUTPUT / "index.bob")
25 changes: 17 additions & 8 deletions src/ibek/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ class Database(BaseSettings):
args: Mapping[str, Optional[str]] = Field(
description=(
"Dictionary of args and values to pass through to database. "
"A value of None is equivalent to ARG: '{{ ARG }}'"
"A value of None is equivalent to ARG: '{{ ARG }}'. "
"See `UTILS.render_map` for more details."
)
)

Expand Down Expand Up @@ -167,17 +168,25 @@ class EntityPVI(BaseSettings):
yaml_path: str = Field(
description="Path to .pvi.device.yaml - absolute or relative to PVI_DEFS"
)
index: bool = Field(
description="Whether to add generated UI to index for Entity", default=True
ui_index: bool = Field(
True,
description="Whether to add the UI to the IOC index.",
)
prefix: str = Field(description="PV prefix to pass as $(prefix) on index button")
pva_template: bool = Field(
ui_macros: dict[str, str | None] = Field(
None,
description=(
"Whether to generate a database template with info tags that create a "
"PVAccess structure defining the PV interface (PVI) for this entity"
"Macros to launch the UI on the IOC index. "
"These must be args of the Entity this is attached to."
),
default=False,
)
pv: bool = Field(
False,
description=(
"Whether to generate a PVI PV. This adds a database template with info "
"tags that create a PVAccess PV representing the device structure."
),
)
pv_prefix: str = Field("", description='PV prefix for PVI PV - e.g. "$(P)"')
Comment on lines +182 to +189
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is also much clearer than the old pva thing



class Definition(BaseSettings):
Expand Down
9 changes: 4 additions & 5 deletions src/ibek/support_cmds/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@
from typing_extensions import Annotated

from ibek.globals import (
IBEK_DEFS,
GLOBALS,
IBEK_GLOBALS,
IOC_DBDS,
IOC_LIBS,
PVI_DEFS,
PVI_YAML_PATTERN,
RELEASE,
RUNTIME_DEBS,
Expand Down Expand Up @@ -260,10 +259,10 @@ def generate_links(
"""
support_globals = folder / ".." / IBEK_GLOBALS

symlink_files(folder, SUPPORT_YAML_PATTERN, IBEK_DEFS)
symlink_files(folder, SUPPORT_YAML_PATTERN, GLOBALS.IBEK_DEFS)
if support_globals.exists():
symlink_files(support_globals, SUPPORT_YAML_PATTERN, IBEK_DEFS)
symlink_files(folder, PVI_YAML_PATTERN, PVI_DEFS)
symlink_files(support_globals, SUPPORT_YAML_PATTERN, GLOBALS.IBEK_DEFS)
symlink_files(folder, PVI_YAML_PATTERN, GLOBALS.PVI_DEFS)


@support_cli.command()
Expand Down
17 changes: 11 additions & 6 deletions src/ibek/support_cmds/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,23 @@ def add_text_once(file: Path, text: str):


def symlink_files(source_directory: Path, file_pattern: str, target_directory: Path):
"""Symlink files patching the given pattern in source directory to target directory.
"""
Symlink files matching the given pattern in source directory to target directory.

Args:
source_directory: Directory containing source files
file_patterm: Pattern of files in source directory to be symlinked
file_pattern: Pattern of files in source directory to be symlinked
target_directory: Directory to create symlinks in

"""
source_files = list(source_directory.glob(file_pattern))
if not source_files:
return

typer.echo(f"Symlinking {file_pattern} files:")
target_directory.mkdir(parents=True, exist_ok=True)
for yaml in source_directory.glob(file_pattern):
typer.echo(f" {target_directory / yaml.name} -> {yaml}")
target = target_directory / yaml.name
for source_file in source_files:
typer.echo(f" {target_directory / source_file.name} -> {source_file}")
target = target_directory / source_file.name
target.unlink(missing_ok=True)
target.symlink_to(yaml)
target.symlink_to(source_file)
Loading
Loading