diff --git a/.claude/skills/db-list/SKILL.md b/.claude/skills/db-list/SKILL.md index 896109874..aedc213ec 100644 --- a/.claude/skills/db-list/SKILL.md +++ b/.claude/skills/db-list/SKILL.md @@ -78,6 +78,7 @@ allowed-tools: | `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) | | `newObjectPosition` | `"end"` / `"byName"` | Куда попадает новый объект в составе конфигурации: в конец своего вида (по умолчанию) или на место по имени. Переопределяется в `databases[]` | | `extensionApplyCheck` | bool | Проверять ли применимость расширения после загрузки в базу (по умолчанию `true`). Разово отключается ключом `-NoApplyCheck` | +| `externalCheck` | bool | Проверять ли исходники внешней обработки/отчёта платформой перед сборкой (по умолчанию `true`). Разово отключается ключом `-Checks off` | | `databases` | array | Массив баз данных | | `default` | string | id базы по умолчанию | diff --git a/.claude/skills/epf-build/SKILL.md b/.claude/skills/epf-build/SKILL.md index 8eb97ffa8..12ab01a0b 100644 --- a/.claude/skills/epf-build/SKILL.md +++ b/.claude/skills/epf-build/SKILL.md @@ -25,7 +25,8 @@ allowed-tools: ## Параметры подключения (опционально) -Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы. +Предпочтительно использовать конкретную базу — это надёжнее. Временная база всё равно поднимается +под проверку исходников, если она не отключена. 1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую @@ -55,11 +56,22 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п | `-Password <пароль>` | нет | Пароль | | `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | | `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу | +| `-Checks <список>` | нет | Что проверить перед сборкой: `modules`, `handlers`, `unreferenced`, `empty-handlers`, `config`; `off` — не проверять. По умолчанию `modules,handlers` | +| `-Context <список>` | нет | Контексты проверки `modules`: `ThinClient`, `Server`, `ExternalConnection`, `ThickClientOrdinaryApplication`. По умолчанию `ThinClient,Server` | | `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` | | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` | > `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных +## Проверка перед сборкой + +Перед сборкой исходники проверяет платформа — синтаксис модулей и наличие обработчиков форм. +Если она нашла проблемы, сборка отменяется и файл не создаётся; в выводе — сообщение +платформы со строкой и колонкой и путь к файлу исходника. Отключается `-Checks off` +или ключом `"externalCheck": false` в `.v8-project.json`. + +Проверка идёт на отдельной временной базе, даже если база указана: чужую конфигурацию навык не трогает. + ## Примеры ```powershell diff --git a/.claude/skills/epf-build/scripts/epf-build.ps1 b/.claude/skills/epf-build/scripts/epf-build.ps1 index 597682861..5dd259092 100644 --- a/.claude/skills/epf-build/scripts/epf-build.ps1 +++ b/.claude/skills/epf-build/scripts/epf-build.ps1 @@ -1,4 +1,4 @@ -# epf-build v1.16 — Build external data processor or report (EPF/ERF) from XML sources +# epf-build v1.17 — Build external data processor or report (EPF/ERF) from XML sources # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -72,6 +72,15 @@ param( [Parameter(Mandatory=$true)] [string]$OutputFile, + # Что проверить в исходниках перед сборкой: modules (синтаксис в контекстах), handlers, + # unreferenced, empty-handlers, config; off — не проверять. По умолчанию modules,handlers. + [Parameter(Mandatory=$false)] + [string]$Checks, + + # Контексты синтаксической проверки. По умолчанию ThinClient,Server. + [Parameter(Mandatory=$false)] + [string]$Context, + [Parameter(Mandatory=$false)] [string[]]$AdditionalV8Arguments = @(), @@ -388,6 +397,122 @@ function Test-OutputNonEmpty { return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0) } +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} + +# --- Проверка исходников платформой --- +# Сборка .epf/.erf ничего не проверяет: /LoadExternalDataProcessorOrReportFromFiles упаковывает XML +# и модули не компилирует, поэтому сломанный модуль доезжает до пользователя и падает при открытии +# обработки. Прямой команды «проверь внешнюю обработку» у платформы нет, но объект КОНФИГУРАЦИИ она +# проверяет — поэтому обработка кладётся в конфигурацию временной базы (stub-db-create +# -EmbedSourceFile) и спрашивается штатной /CheckConfig. +# +# Запуск отдельный и только через 1cv8: у ibcmd такой команды нет, а в одной командной строке +# DESIGNER выполняет лишь последнюю пакетную команду. +function Get-CheckFlags { + param([string[]]$Checks, [string[]]$Contexts) + $flags = @() + if ($Checks -contains 'modules') { foreach ($c in $Contexts) { $flags += "-$c" } } + if ($Checks -contains 'handlers') { $flags += '-HandlersExistence' } + if ($Checks -contains 'unreferenced') { $flags += '-UnreferenceProcedures' } + if ($Checks -contains 'empty-handlers') { $flags += '-EmptyHandlers' } + if ($Checks -contains 'config') { $flags += '-ConfigLogIntegrity', '-IncorrectReferences' } + return $flags +} + +# Платформа называет объект своим именем внутри конфигурации; модели нужен путь к исходнику. +# Путь СКЛЕИВАЕТСЯ по конвенции выгрузки, поэтому возвращается только существующий файл: +# выдуманный путь хуже отсутствующего — модель пойдёт открывать файл, которого нет. +function Resolve-SourcePath { + param([string]$Line, [string]$SourceDir) + $candidate = $null + $m = [regex]::Match($Line, '(?:Обработка|Отчет|DataProcessor|Report)\.([^.]+)\.(?:Форма|Form)\.([^.]+)\.') + if ($m.Success) { $candidate = (Join-Path $SourceDir (Join-Path $m.Groups[1].Value (Join-Path "Forms" (Join-Path $m.Groups[2].Value "Ext\Form\Module.bsl")))) } + if (-not $candidate) { + $m = [regex]::Match($Line, '(?:Обработка|Отчет|DataProcessor|Report)\.([^.]+)\.(МодульОбъекта|ObjectModule)') + if ($m.Success) { $candidate = (Join-Path $SourceDir (Join-Path $m.Groups[1].Value "Ext\ObjectModule.bsl")) } + } + if (-not $candidate) { + $m = [regex]::Match($Line, '(?:Обработка|Отчет|DataProcessor|Report)\.([^.]+)\.(МодульМенеджера|ManagerModule)') + if ($m.Success) { $candidate = (Join-Path $SourceDir (Join-Path $m.Groups[1].Value "Ext\ManagerModule.bsl")) } + } + if ($candidate -and (Test-Path $candidate -PathType Leaf)) { return $candidate } + return $null +} + +# $true, если платформа нашла проблемы — вызывающий не собирает артефакт. +function Invoke-SourceCheck { + param([string]$Exe, [string]$BasePath, [string[]]$Flags, [string]$SourceDir, [string[]]$ExtraArgs) + $exeDir = Split-Path $Exe -Parent + $exeLeaf = Split-Path $Exe -Leaf + $v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe } + if (-not (Test-Path $v8)) { + Write-Host "[note] source check skipped: 1cv8 not found at $v8" -ForegroundColor Yellow + return $false + } + $dir = Join-Path $env:TEMP "epf_check_$(Get-Random)" + New-Item -ItemType Directory -Path $dir -Force | Out-Null + try { + $outFile = Join-Path $dir "check_log.txt" + $a = @("DESIGNER", "/F", "`"$BasePath`"", "/CheckConfig") + $Flags + @("/Out", "`"$outFile`"", "/DisableStartupDialogs") + $ExtraArgs + Write-Host "Running: 1cv8.exe $($a -join ' ')" + $res = Invoke-PlatformProcess $v8 $a -PreQuoted + $lines = @() + if (Test-Path $outFile) { + $raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue + if ($raw) { $lines = @($raw -split "`r?`n" | Where-Object { $_.Trim() -ne '' }) } + } + # Платформа отвечает 101 на найденные проблемы; «Ошибок не обнаружено» приходит с кодом 0. + if ($res.ExitCode -eq 0) { return $false } + Write-Host "Error: платформа нашла проблемы в исходниках — сборка отменена" -ForegroundColor Red + # Пустой лог при ненулевом коде — отказ не по находкам (база занята, нет лицензии); молчать нельзя. + if ($lines.Count -eq 0) { Write-Host " платформа вернула код $($res.ExitCode) без сообщений" -ForegroundColor Red } + foreach ($l in $lines) { + Write-Host " $($l.TrimEnd())" -ForegroundColor Red + $srcPath = Resolve-SourcePath $l $SourceDir + if ($srcPath) { Write-Host " -> $srcPath" -ForegroundColor Red } + } + return $true + } finally { + if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue } + } +} + +# Проверять ли исходники: -Checks off сильнее настройки проекта externalCheck. +function Get-SourceCheckList { + param([string]$Checks) + $known = @('modules', 'handlers', 'unreferenced', 'empty-handlers', 'config') + if ($Checks) { + $list = @($Checks -split ',' | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ }) + if ($list -contains 'off') { return @() } + foreach ($c in $list) { + if ($known -notcontains $c) { + Write-Host "Error: unknown check '$c' (expected: $($known -join ', ') or off)" -ForegroundColor Red + exit 1 + } + } + return $list + } + $pf = Find-V8Project (Get-Location).Path + if ($pf) { + try { + $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json + if ($null -ne $proj.externalCheck -and -not [bool]$proj.externalCheck) { return @() } + } catch {} + } + return @('modules', 'handlers') +} + + $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } # --- Resolve additional arguments for the selected engine --- @@ -398,33 +523,69 @@ if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) { exit 1 } -# --- Auto-create stub database if no connection specified --- -$autoCreatedBase = $null -if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { - $sourceDir = Split-Path $SourceFile -Parent - $autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)" +# --- Что проверяем в исходниках перед сборкой --- +$checkList = @(Get-SourceCheckList $Checks) +$contextList = @() +if ($Context) { $contextList = @($Context -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } +if ($contextList.Count -eq 0) { $contextList = @('ThinClient', 'Server') } +elseif ($checkList.Count -gt 0 -and $checkList -notcontains 'modules') { + Write-Host "Error: -Context задан, но в -Checks нет modules — контексты относятся только к ней" -ForegroundColor Red + exit 1 +} +$sourceDir = Split-Path $SourceFile -Parent + +function New-StubBase { + # Стаб запускает свои процессы платформы (CREATEINFOBASE, LoadConfigFromFiles, UpdateDBCfg) — + # им нужны те же дополнительные аргументы, что и сборке. Передаются только явные: файл проекта + # стаб читает сам. Вызов через -Command, не -File: -File берёт хвост буквально, и массивный + # параметр пришёл бы одним склеенным токеном. + param([string]$BasePath, [switch]$Embed) $stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1" - Write-Host "No database specified. Creating temporary stub database..." - # The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles, - # UpdateDBCfg) — they need the same extra arguments as the final build. Only the - # explicit ones are forwarded: the stub reads .v8-project.json itself. - # Invoked via -Command, not -File: -File takes the tail literally, so an array - # parameter would arrive as a single comma-glued token. $q = { param($s) "'" + ($s -replace "'", "''") + "'" } - $stubCmd = "& $(& $q $stubScript) -SourceDir $(& $q $sourceDir) -V8Path $(& $q $V8Path) -TempBasePath $(& $q $autoBasePath)" + $stubCmd = "& $(& $q $stubScript) -SourceDir $(& $q $sourceDir) -V8Path $(& $q $V8Path) -TempBasePath $(& $q $BasePath)" + if ($Embed) { $stubCmd += " -EmbedSourceFile $(& $q $SourceFile)" } if ($AdditionalV8Arguments.Count -gt 0) { $stubCmd += " -AdditionalV8Arguments " + (($AdditionalV8Arguments | ForEach-Object { & $q $_ }) -join ',') } if ($AdditionalIbcmdArguments.Count -gt 0) { $stubCmd += " -AdditionalIbcmdArguments " + (($AdditionalIbcmdArguments | ForEach-Object { & $q $_ }) -join ',') } - $stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -Command `"$stubCmd`"" -NoNewWindow -Wait -PassThru - if ($stubProc.ExitCode -ne 0) { - Write-Host "Error: failed to create stub database" -ForegroundColor Red + $p = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -Command `"$stubCmd`"" -NoNewWindow -Wait -PassThru + return $p.ExitCode +} + +# --- Auto-create stub database if no connection specified --- +$autoCreatedBase = $null +$checkBase = $null +$checkBasePath = $null +if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { + $autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)" + Write-Host "No database specified. Creating temporary stub database..." + if ((New-StubBase $autoBasePath -Embed:($checkList.Count -gt 0)) -ne 0) { + # С внедрённой обработкой база падает прежде всего из-за самих исходников + # (пример: DefaultForm на несуществующую форму) — говорить про базу значит увести не туда. + if ($checkList.Count -gt 0) { + Write-Host "Error: платформа не приняла исходники при подготовке проверки — сборка отменена" -ForegroundColor Red + Write-Host " сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт" -ForegroundColor Red + } else { + Write-Host "Error: failed to create stub database" -ForegroundColor Red + } exit 1 } $InfoBasePath = $autoBasePath $autoCreatedBase = $autoBasePath + if ($checkList.Count -gt 0) { $checkBasePath = $autoBasePath } +} elseif ($checkList.Count -gt 0) { + # Базу указали снаружи: класть проверяемую обработку в чужую конфигурацию нельзя, поэтому под + # проверку поднимается своя временная база, а сборка идёт на указанной. + $checkBase = Join-Path $env:TEMP "epf_check_db_$(Get-Random)" + Write-Host "Creating temporary database for the source check..." + if ((New-StubBase $checkBase -Embed) -ne 0) { + Write-Host "Error: платформа не приняла исходники при подготовке проверки — сборка отменена" -ForegroundColor Red + Write-Host " сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт" -ForegroundColor Red + exit 1 + } + $checkBasePath = $checkBase } # --- Validate source file --- @@ -433,6 +594,18 @@ if (-not (Test-Path $SourceFile)) { exit 1 } +# --- Проверка исходников платформой: сломанный .epf до пользователя доезжать не должен --- +if ($checkList.Count -gt 0 -and $checkBasePath) { + # Проверку ведёт 1cv8, поэтому ibcmd-шные дополнительные аргументы ей не отдаём. + $checkExtra = if ($engine -eq "ibcmd") { @() } else { $extraArgs } + $found = Invoke-SourceCheck $V8Path $checkBasePath (Get-CheckFlags $checkList $contextList) $sourceDir $checkExtra + if ($found) { + if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) { Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue } + if ($checkBase -and (Test-Path $checkBase)) { Remove-Item -Path $checkBase -Recurse -Force -ErrorAction SilentlyContinue } + exit 1 + } +} + # --- Ensure output directory exists --- $outDir = Split-Path $OutputFile -Parent if ($outDir -and -not (Test-Path $outDir)) { @@ -526,4 +699,7 @@ try { if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) { Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue } + if ($checkBase -and (Test-Path $checkBase)) { + Remove-Item -Path $checkBase -Recurse -Force -ErrorAction SilentlyContinue + } } diff --git a/.claude/skills/epf-build/scripts/epf-build.py b/.claude/skills/epf-build/scripts/epf-build.py index 709b23138..f7a41a560 100644 --- a/.claude/skills/epf-build/scripts/epf-build.py +++ b/.claude/skills/epf-build/scripts/epf-build.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 -# epf-build v1.16 — Build external data processor or report (EPF/ERF) from XML sources +# epf-build v1.17 — Build external data processor or report (EPF/ERF) from XML sources # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import atexit import glob +import io import json import os import random @@ -410,6 +411,136 @@ def _redact(text, *secrets): return text +def _sg_find_v8project(start_dir): + d = start_dir + for _ in range(20): + if not d: + break + pj = os.path.join(d, ".v8-project.json") + if os.path.isfile(pj): + return pj + parent = os.path.dirname(d) + if parent == d: + break + d = parent + return None + + +# --- Проверка исходников платформой --- +# Сборка .epf/.erf ничего не проверяет: /LoadExternalDataProcessorOrReportFromFiles упаковывает XML +# и модули не компилирует, поэтому сломанный модуль доезжает до пользователя и падает при открытии +# обработки. Прямой команды «проверь внешнюю обработку» у платформы нет, но объект КОНФИГУРАЦИИ она +# проверяет — поэтому обработка кладётся в конфигурацию временной базы (stub-db-create +# -EmbedSourceFile) и спрашивается штатной /CheckConfig. +# +# Запуск отдельный и только через 1cv8: у ibcmd такой команды нет, а в одной командной строке +# DESIGNER выполняет лишь последнюю пакетную команду. +def get_check_flags(checks, contexts): + flags = [] + if 'modules' in checks: + for c in contexts: + flags.append('-' + c) + if 'handlers' in checks: + flags.append('-HandlersExistence') + if 'unreferenced' in checks: + flags.append('-UnreferenceProcedures') + if 'empty-handlers' in checks: + flags.append('-EmptyHandlers') + if 'config' in checks: + flags += ['-ConfigLogIntegrity', '-IncorrectReferences'] + return flags + + +# Платформа называет объект своим именем внутри конфигурации; модели нужен путь к исходнику. +# Путь СКЛЕИВАЕТСЯ по конвенции выгрузки, поэтому возвращается только существующий файл: +# выдуманный путь хуже отсутствующего — модель пойдёт открывать файл, которого нет. +def resolve_source_path(line, source_dir): + candidate = None + m = re.search(r'(?:\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430|\u041e\u0442\u0447\u0435\u0442|DataProcessor|Report)\.([^.]+)\.(?:\u0424\u043e\u0440\u043c\u0430|Form)\.([^.]+)\.', line) + if m: + candidate = os.path.join(source_dir, m.group(1), 'Forms', m.group(2), 'Ext', 'Form', 'Module.bsl') + if candidate is None: + m = re.search(r'(?:\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430|\u041e\u0442\u0447\u0435\u0442|DataProcessor|Report)\.([^.]+)\.(?:\u041c\u043e\u0434\u0443\u043b\u044c\u041e\u0431\u044a\u0435\u043a\u0442\u0430|ObjectModule)', line) + if m: + candidate = os.path.join(source_dir, m.group(1), 'Ext', 'ObjectModule.bsl') + if candidate is None: + m = re.search(r'(?:\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430|\u041e\u0442\u0447\u0435\u0442|DataProcessor|Report)\.([^.]+)\.(?:\u041c\u043e\u0434\u0443\u043b\u044c\u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430|ManagerModule)', line) + if m: + candidate = os.path.join(source_dir, m.group(1), 'Ext', 'ManagerModule.bsl') + if candidate and os.path.isfile(candidate): + return candidate + return None + + +# True, если платформа нашла проблемы — вызывающий не собирает артефакт. +def invoke_source_check(exe, base_path, flags, source_dir, extra_args): + exe_dir = os.path.dirname(exe) + exe_leaf = os.path.basename(exe) + if exe_leaf.lower().startswith('ibcmd'): + v8 = os.path.join(exe_dir, '1cv8' + os.path.splitext(exe)[1]) + else: + v8 = exe + if not os.path.exists(v8): + print(f'[note] source check skipped: 1cv8 not found at {v8}') + return False + d = os.path.join(tempfile.gettempdir(), f'epf_check_{random.randint(0, 999999)}') + os.makedirs(d, exist_ok=True) + try: + out_file = os.path.join(d, 'check_log.txt') + a = (['DESIGNER', '/F', f'"{base_path}"', '/CheckConfig'] + flags + + ['/Out', f'"{out_file}"', '/DisableStartupDialogs'] + + [quote_if_needed(x) for x in extra_args]) + print(f'Running: 1cv8.exe {" ".join(a)}') + result = run_v8(v8, a) + lines = [] + if os.path.isfile(out_file): + try: + with io.open(out_file, encoding='utf-8-sig', errors='replace') as fh: + raw = fh.read() + lines = [l for l in raw.splitlines() if l.strip()] + except Exception: + lines = [] + # Платформа отвечает 101 на найденные проблемы; «Ошибок не обнаружено» приходит с кодом 0. + if result.returncode == 0: + return False + print('Error: \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u043d\u0430\u0448\u043b\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0432 \u0438\u0441\u0445\u043e\u0434\u043d\u0438\u043a\u0430\u0445 \u2014 \u0441\u0431\u043e\u0440\u043a\u0430 \u043e\u0442\u043c\u0435\u043d\u0435\u043d\u0430') + # Пустой лог при ненулевом коде — отказ не по находкам (база занята, нет лицензии); молчать нельзя. + if not lines: + print(f' платформа вернула код {result.returncode} без сообщений') + for l in lines: + print(f' {l.rstrip()}') + src_path = resolve_source_path(l, source_dir) + if src_path: + print(f' -> {src_path}') + return True + finally: + shutil.rmtree(d, ignore_errors=True) + + +# Проверять ли исходники: -Checks off сильнее настройки проекта externalCheck. +def get_source_check_list(checks): + known = ['modules', 'handlers', 'unreferenced', 'empty-handlers', 'config'] + if checks: + lst = [c.strip().lower() for c in checks.split(',') if c.strip()] + if 'off' in lst: + return [] + for c in lst: + if c not in known: + print(f'Error: unknown check \'{c}\' (expected: {", ".join(known)} or off)') + sys.exit(1) + return lst + pf = _sg_find_v8project(os.getcwd()) + if pf: + try: + with open(pf, encoding='utf-8-sig') as fh: + proj = json.load(fh) + if 'externalCheck' in proj and not proj['externalCheck']: + return [] + except Exception: + pass + return ['modules', 'handlers'] + + def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") @@ -425,6 +556,11 @@ def main(): parser.add_argument("-Password", default="", help="1C user password") parser.add_argument("-SourceFile", required=True, help="Path to root XML source file") parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file") + # Что проверить в исходниках перед сборкой: modules (синтаксис в контекстах), handlers, + # unreferenced, empty-handlers, config; off — не проверять. По умолчанию modules,handlers. + parser.add_argument("-Checks", default="") + # Контексты синтаксической проверки. По умолчанию ThinClient,Server. + parser.add_argument("-Context", default="") parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], @@ -458,34 +594,80 @@ def main(): print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)") sys.exit(1) - # --- Auto-create stub database if no connection specified --- - auto_created_base = None - if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): - source_dir = os.path.dirname(os.path.abspath(args.SourceFile)) - auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}") - stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py") - print("No database specified. Creating temporary stub database...") - stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path, - "-TempBasePath", auto_base_path] + # --- Что проверяем в исходниках перед сборкой --- + check_list = get_source_check_list(args.Checks) + context_list = [c.strip() for c in args.Context.split(',') if c.strip()] if args.Context else [] + if not context_list: + context_list = ['ThinClient', 'Server'] + elif check_list and 'modules' not in check_list: + print('Error: -Context задан, но в -Checks нет modules — контексты относятся только к ней') + sys.exit(1) + source_dir = os.path.dirname(os.path.abspath(args.SourceFile)) + + def new_stub_base(base_path, embed): # The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles, # UpdateDBCfg) — they need the same extra arguments as the final build. Only the # explicit ones are forwarded: the stub reads .v8-project.json itself. + stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py") + stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path, + "-TempBasePath", base_path] + if embed: + stub_cmd += ["-EmbedSourceFile", args.SourceFile] if v8_extra: stub_cmd += ["-AdditionalV8Arguments"] + list(v8_extra) if ibcmd_extra: stub_cmd += ["-AdditionalIbcmdArguments"] + list(ibcmd_extra) - result = subprocess.run(stub_cmd, capture_output=False) - if result.returncode != 0: - print("Error: failed to create stub database") + return subprocess.run(stub_cmd, capture_output=False).returncode + + # --- Auto-create stub database if no connection specified --- + auto_created_base = None + check_base = None + check_base_path = None + if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): + auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}") + print("No database specified. Creating temporary stub database...") + if new_stub_base(auto_base_path, bool(check_list)) != 0: + # С внедрённой обработкой база падает прежде всего из-за самих исходников + # (пример: DefaultForm на несуществующую форму) — говорить про базу значит увести не туда. + if check_list: + print('Error: платформа не приняла исходники при подготовке проверки — сборка отменена') + print(' сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт') + else: + print("Error: failed to create stub database") sys.exit(1) args.InfoBasePath = auto_base_path auto_created_base = auto_base_path + if check_list: + check_base_path = auto_base_path + elif check_list: + # Базу указали снаружи: класть проверяемую обработку в чужую конфигурацию нельзя, поэтому под + # проверку поднимается своя временная база, а сборка идёт на указанной. + check_base = os.path.join(tempfile.gettempdir(), f"epf_check_db_{random.randint(0, 999999)}") + print("Creating temporary database for the source check...") + if new_stub_base(check_base, True) != 0: + print('Error: платформа не приняла исходники при подготовке проверки — сборка отменена') + print(' сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт') + sys.exit(1) + check_base_path = check_base # --- Validate source file --- if not os.path.isfile(args.SourceFile): print(f"Error: source file not found: {args.SourceFile}") sys.exit(1) + # --- Проверка исходников платформой: сломанный .epf до пользователя доезжать не должен --- + if check_list and check_base_path: + # Проверку ведёт 1cv8, поэтому ibcmd-шные дополнительные аргументы ей не отдаём. + check_extra = [] if engine == "ibcmd" else extra_args + found = invoke_source_check(v8path, check_base_path, + get_check_flags(check_list, context_list), source_dir, check_extra) + if found: + if auto_created_base and os.path.exists(auto_created_base): + shutil.rmtree(auto_created_base, ignore_errors=True) + if check_base and os.path.exists(check_base): + shutil.rmtree(check_base, ignore_errors=True) + sys.exit(1) + # --- Ensure output directory exists --- out_dir = os.path.dirname(args.OutputFile) if out_dir and not os.path.exists(out_dir): @@ -579,6 +761,8 @@ def main(): shutil.rmtree(temp_dir, ignore_errors=True) if auto_created_base and os.path.exists(auto_created_base): shutil.rmtree(auto_created_base, ignore_errors=True) + if check_base and os.path.exists(check_base): + shutil.rmtree(check_base, ignore_errors=True) if __name__ == "__main__": diff --git a/.claude/skills/epf-build/scripts/stub-db-create.ps1 b/.claude/skills/epf-build/scripts/stub-db-create.ps1 index 06dd8c571..c36a95029 100644 --- a/.claude/skills/epf-build/scripts/stub-db-create.ps1 +++ b/.claude/skills/epf-build/scripts/stub-db-create.ps1 @@ -1,4 +1,4 @@ -# stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build +# stub-db-create v1.9 — Create temp 1C infobase with metadata stubs for EPF/ERF build # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -9,6 +9,10 @@ param( [string]$TempBasePath, + # XML проверяемой обработки/отчёта: объект кладётся в конфигурацию-заглушку, чтобы платформа + # смогла проверить его штатными проверками. Без параметра стаб работает как раньше. + [string]$EmbedSourceFile, + [string[]]$AdditionalV8Arguments = @(), [string[]]$AdditionalIbcmdArguments = @() @@ -350,6 +354,9 @@ foreach ($f in $xmlFiles) { } $hasRefTypes = $typeMap.Count -gt 0 +# Конфигурация нужна и тогда, когда ссылочных типов нет: в неё кладётся сам объект. +$embedRequested = -not [string]::IsNullOrWhiteSpace($EmbedSourceFile) +$needCfg = $hasRefTypes -or $embedRequested # --- 2. Determine TempBasePath --- if (-not $TempBasePath) { @@ -370,13 +377,106 @@ if ($needsRegistrator) { $typeMap["Document"]["ЗаглушкаРегистратора"] = $true } +# --- Внедрение проверяемого объекта в конфигурацию-заглушку --- +# Платформа не умеет проверять внешнюю обработку: /LoadExternalDataProcessorOrReportFromFiles +# только упаковывает XML и модули не компилирует. Зато она проверяет объект КОНФИГУРАЦИИ, а +# внешняя обработка отличается от него немногим (замер 8.3.24): корневым тегом, именем +# порождаемого объектного типа и отсутствием типа менеджера. Правим ровно эти точки и переносим +# остальное как есть — под проверку попадает всё, что написал автор, включая реквизиты, формы и +# макеты, а формат может расти без правок здесь. +# +# Подстановка типа делается ТОЛЬКО в .xml (это DefaultForm и основной реквизит формы); в .bsl +# такой же текст был бы кодом, и трогать его нельзя. +function Add-SourceObjectToConfig { + param([string]$SourceXml, [string]$CfgDir) + + # Копия объекта живёт в конфигурации базы, а следом в ту же базу грузится исходник как ВНЕШНЯЯ + # обработка. С одинаковыми идентификаторами платформа путает их и через раз отвечает «Исключение + # XDTO при чтении файла» на исправном исходнике — поэтому у копии все GUID свои, но согласованные + # между её файлами (ссылки внутри объекта идут по идентификатору). + $guidMap = @{} + $reGuid = [regex]'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' + $reissue = { + param($m) + $k = $m.Value.ToLower() + if (-not $guidMap.ContainsKey($k)) { $guidMap[$k] = [guid]::NewGuid().ToString() } + $guidMap[$k] + } + + $text = [IO.File]::ReadAllText($SourceXml, [Text.Encoding]::UTF8) + if ($text -match ']') { + $extTag = 'ExternalDataProcessor'; $cfgTag = 'DataProcessor'; $folder = 'DataProcessors' + } elseif ($text -match ']') { + $extTag = 'ExternalReport'; $cfgTag = 'Report'; $folder = 'Reports' + } else { + return $null + } + + $name = if ($text -match '([^<]+)') { $Matches[1] } else { [IO.Path]::GetFileNameWithoutExtension($SourceXml) } + + $conv = $reGuid.Replace($text, $reissue) + $conv = $conv.Replace("<$extTag ", "<$cfgTag ").Replace("<$extTag>", "<$cfgTag>").Replace("", "") + $conv = $conv.Replace("${extTag}Object.", "${cfgTag}Object.").Replace("$extTag.", "$cfgTag.") + + # Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен: + # без него платформа отвечает «отсутствует один или более типов объекта». + $mgr = "`t`t`t`r`n" + + "`t`t`t`t$([guid]::NewGuid().ToString())`r`n" + + "`t`t`t`t$([guid]::NewGuid().ToString())`r`n" + + "`t`t`t`r`n" + if ($conv -match '') { + $conv = [regex]::Replace($conv, '(\s*)', ("`r`n" + $mgr + "`t`t"), 1) + } else { + $objType = "`t`t`t`r`n" + + "`t`t`t`t$([guid]::NewGuid().ToString())`r`n" + + "`t`t`t`t$([guid]::NewGuid().ToString())`r`n" + + "`t`t`t`r`n" + $conv = [regex]::Replace($conv, "(<$cfgTag[^>]*>)", ("`$1`r`n`t`t`r`n" + $objType + $mgr + "`t`t"), 1) + } + + $objDir = Join-Path $CfgDir $folder + New-Item -ItemType Directory -Path $objDir -Force | Out-Null + $encBom = New-Object System.Text.UTF8Encoding($true) + [IO.File]::WriteAllText((Join-Path $objDir "$name.xml"), $conv, $encBom) + + # Содержимое объекта — как есть; в XML та же подстановка типа, .bsl копируются байт в байт. + $srcContent = Join-Path (Split-Path $SourceXml -Parent) $name + if (Test-Path $srcContent) { + $dstContent = Join-Path $objDir $name + foreach ($f in (Get-ChildItem -Path $srcContent -Recurse -File)) { + $rel = $f.FullName.Substring($srcContent.Length).TrimStart('\', '/') + $dst = Join-Path $dstContent $rel + New-Item -ItemType Directory -Path (Split-Path $dst -Parent) -Force | Out-Null + if ($f.Extension -ieq '.xml') { + $t = [IO.File]::ReadAllText($f.FullName, [Text.Encoding]::UTF8) + $t = $reGuid.Replace($t, $reissue) + $t = $t.Replace("${extTag}Object.", "${cfgTag}Object.").Replace("$extTag.", "$cfgTag.") + [IO.File]::WriteAllText($dst, $t, $encBom) + } else { + Copy-Item -Path $f.FullName -Destination $dst -Force + } + } + } + + return @{ Tag = $cfgTag; Name = $name } +} + # --- 4. Generate configuration XML --- -if ($hasRefTypes) { +if ($needCfg) { $enc = New-Object System.Text.UTF8Encoding($true) $cfgDir = Join-Path $TempBasePath "cfg" New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null + $embedded = $null + if ($embedRequested) { + $embedded = Add-SourceObjectToConfig $EmbedSourceFile $cfgDir + if (-not $embedded) { + Write-Host "Error: $EmbedSourceFile is neither ExternalDataProcessor nor ExternalReport" -ForegroundColor Red + exit 1 + } + } + # Заглушке нужна САМАЯ НИЗКАЯ работающая версия, а не версия исходников: ограничение платформы # одностороннее — она читает формат не новее себя. Отсюда min(версия исходников, 2.17): на 2.17+ # заглушка остаётся 2.17 (как было), а под исходники 2.13-2.16 опускается до их версии, иначе @@ -566,6 +666,7 @@ if ($hasRefTypes) { $childXml += "`r`n`t`t`t<$tag>$name" } } + if ($embedded) { $childXml += "`r`n`t`t`t<$($embedded.Tag)>$($embedded.Name)" } $cfgXml = @" @@ -1548,7 +1649,7 @@ if ($stubEngine -eq "ibcmd") { $ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)" New-Item -ItemType Directory -Path $ibData -Force | Out-Null $ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database") - if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" } + if ($needCfg) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" } $ibArgs += "--data=$ibData" $ibArgs += $extraArgs $__ib = Invoke-PlatformProcess $V8Path $ibArgs @@ -1560,7 +1661,7 @@ if ($stubEngine -eq "ibcmd") { Write-Error "Failed to create stub infobase (code: $ibRc)" exit 1 } - if ($hasRefTypes) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue } + if ($needCfg) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue } Write-Host "[OK] Stub database created: $TempBasePath" Write-Host $TempBasePath exit 0 @@ -1576,8 +1677,8 @@ if ($proc.ExitCode -ne 0) { exit 1 } -# --- 6. Load config and update DB if ref types exist --- -if ($hasRefTypes) { +# --- 6. Load config and update DB if there is one --- +if ($needCfg) { $cfgDir = Join-Path $TempBasePath "cfg" # LoadConfigFromFiles Write-Host "Loading configuration from files..." diff --git a/.claude/skills/epf-build/scripts/stub-db-create.py b/.claude/skills/epf-build/scripts/stub-db-create.py index 54af25564..8b8871968 100644 --- a/.claude/skills/epf-build/scripts/stub-db-create.py +++ b/.claude/skills/epf-build/scripts/stub-db-create.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 -# stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build +# stub-db-create v1.9 — Create temp 1C infobase with metadata stubs for EPF/ERF build # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse +import io import os +import shutil import random import re import subprocess @@ -64,11 +66,31 @@ def run_v8(v8path, arguments): The arguments carry their own quotes inside the value (File="C:\\a b") — that is where 1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would escape those quotes, so there the command line is handed over ready-made. + + На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы + частью значения: путь с пробелом платформа не находит («Неопределена информационная + база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой + обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь", + File="…") не задеты: у них кавычки внутри токена, а не по краям. """ if os.name == "nt": cmd = '"' + v8path + '" ' + " ".join(arguments) else: - cmd = [v8path] + arguments + def strip_framing_quotes(a): + # Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX + # становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным + # токеном даёт «Неопределена информационная база», а склеенный + # /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»; + # без кавычек обе формы работают. + if len(a) > 1 and a[0] == '"' and a[-1] == '"': + return a[1:-1] # "значение" отдельным токеном + if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]: + i = a.index('"') + return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя + return a # File="…" не трогаем: там кавычки — + # часть синтаксиса строки соединения, + # и с ними на POSIX всё работает + cmd = [v8path] + [strip_framing_quotes(a) for a in arguments] r = subprocess.run(cmd, input=b"", capture_output=True) r.stdout = decode_platform_bytes(r.stdout) r.stderr = decode_platform_bytes(r.stderr) @@ -178,7 +200,6 @@ def assert_extra_args(extra, engine, hints): print( f"Error: '{tok}' is a positional token — pass values as --key=value " f"({param} cannot extend the ibcmd command)", - file=sys.stderr, ) sys.exit(1) if engine != "ibcmd": @@ -187,7 +208,6 @@ def assert_extra_args(extra, engine, hints): print( f"Error: {b} is a batch command; passed via {param} it would replace " f"the skill's own operation (a command line runs only its last batch command)", - file=sys.stderr, ) sys.exit(1) for k in owned: @@ -195,7 +215,6 @@ def assert_extra_args(extra, engine, hints): hint = f" (use {hints[k]})" if hints and k in hints else "" print( f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}", - file=sys.stderr, ) sys.exit(1) @@ -263,14 +282,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints): print( "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd " "(use -AdditionalIbcmdArguments)", - file=sys.stderr, ) sys.exit(1) if engine != "ibcmd" and ibcmd_extra: print( "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 " "(use -AdditionalV8Arguments)", - file=sys.stderr, ) sys.exit(1) if engine == "ibcmd": @@ -1103,6 +1120,94 @@ def write_bom(path, content): f.write(content) +# --- Внедрение проверяемого объекта в конфигурацию-заглушку --- +# Платформа не умеет проверять внешнюю обработку: /LoadExternalDataProcessorOrReportFromFiles +# только упаковывает XML и модули не компилирует. Зато она проверяет объект КОНФИГУРАЦИИ, а +# внешняя обработка отличается от него немногим (замер 8.3.24): корневым тегом, именем +# порождаемого объектного типа и отсутствием типа менеджера. Правим ровно эти точки и переносим +# остальное как есть — под проверку попадает всё, что написал автор, включая реквизиты, формы и +# макеты, а формат может расти без правок здесь. +# +# Подстановка типа делается ТОЛЬКО в .xml (это DefaultForm и основной реквизит формы); в .bsl +# такой же текст был бы кодом, и трогать его нельзя. +GUID_RE = re.compile(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') + + +def add_source_object_to_config(source_xml, cfg_dir): + # Копия объекта живёт в конфигурации базы, а следом в ту же базу грузится исходник как ВНЕШНЯЯ + # обработка. С одинаковыми идентификаторами платформа путает их и через раз отвечает «Исключение + # XDTO при чтении файла» на исправном исходнике — поэтому у копии все GUID свои, но согласованные + # между её файлами (ссылки внутри объекта идут по идентификатору). + guid_map = {} + + def reissue(text): + def sub(m): + k = m.group(0).lower() + if k not in guid_map: + guid_map[k] = new_uuid() + return guid_map[k] + return GUID_RE.sub(sub, text) + + with io.open(source_xml, encoding='utf-8-sig') as fh: + text = fh.read() + if re.search(r']', text): + ext_tag, cfg_tag, folder = 'ExternalDataProcessor', 'DataProcessor', 'DataProcessors' + elif re.search(r']', text): + ext_tag, cfg_tag, folder = 'ExternalReport', 'Report', 'Reports' + else: + return None + + m = re.search(r'([^<]+)', text) + name = m.group(1) if m else os.path.splitext(os.path.basename(source_xml))[0] + + conv = reissue(text) + conv = conv.replace('<%s ' % ext_tag, '<%s ' % cfg_tag).replace('<%s>' % ext_tag, '<%s>' % cfg_tag) + conv = conv.replace('' % ext_tag, '' % cfg_tag) + conv = conv.replace('%sObject.' % ext_tag, '%sObject.' % cfg_tag).replace('%s.' % ext_tag, '%s.' % cfg_tag) + + # Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен: + # без него платформа отвечает «отсутствует один или более типов объекта». + mgr = ('\t\t\t\r\n' % (cfg_tag, name) + + '\t\t\t\t%s\r\n' % new_uuid() + + '\t\t\t\t%s\r\n' % new_uuid() + + '\t\t\t\r\n') + if '' in conv: + conv = re.sub(r'\s*', lambda _m: '\r\n' + mgr + '\t\t', conv, count=1) + else: + obj_type = ('\t\t\t\r\n' % (cfg_tag, name) + + '\t\t\t\t%s\r\n' % new_uuid() + + '\t\t\t\t%s\r\n' % new_uuid() + + '\t\t\t\r\n') + conv = re.sub('(<%s[^>]*>)' % cfg_tag, + lambda m2: m2.group(1) + '\r\n\t\t\r\n' + obj_type + mgr + '\t\t', + conv, count=1) + + obj_dir = os.path.join(cfg_dir, folder) + os.makedirs(obj_dir, exist_ok=True) + write_bom(os.path.join(obj_dir, '%s.xml' % name), conv) + + # Содержимое объекта — как есть; в XML та же подстановка типа, .bsl копируются байт в байт. + src_content = os.path.join(os.path.dirname(source_xml), name) + if os.path.isdir(src_content): + dst_content = os.path.join(obj_dir, name) + for root, _dirs, files in os.walk(src_content): + for fname in files: + full = os.path.join(root, fname) + rel = os.path.relpath(full, src_content) + dst = os.path.join(dst_content, rel) + os.makedirs(os.path.dirname(dst), exist_ok=True) + if os.path.splitext(fname)[1].lower() == '.xml': + with io.open(full, encoding='utf-8-sig') as fh: + t = fh.read() + t = reissue(t) + t = t.replace('%sObject.' % ext_tag, '%sObject.' % cfg_tag).replace('%s.' % ext_tag, '%s.' % cfg_tag) + write_bom(dst, t) + else: + shutil.copyfile(full, dst) + + return {'tag': cfg_tag, 'name': name} + + def main(): sys.stdout.reconfigure(encoding='utf-8') sys.stderr.reconfigure(encoding='utf-8') @@ -1111,6 +1216,9 @@ def main(): parser.add_argument('-SourceDir', required=True) parser.add_argument('-V8Path', required=True) parser.add_argument('-TempBasePath', default='') + # XML проверяемой обработки/отчёта: объект кладётся в конфигурацию-заглушку, чтобы платформа + # смогла проверить его штатными проверками. Без параметра стаб работает как раньше. + parser.add_argument('-EmbedSourceFile', default='') parser.add_argument('-AdditionalV8Arguments', nargs='*', default=[], help='Extra 1cv8 arguments, e.g. /UseHwLicenses+') parser.add_argument('-AdditionalIbcmdArguments', nargs='*', default=[], @@ -1122,10 +1230,13 @@ def main(): args.SourceDir = clean_path(args.SourceDir, "-SourceDir") args.V8Path = clean_path(args.V8Path, "-V8Path") args.TempBasePath = clean_path(args.TempBasePath, "-TempBasePath") + args.EmbedSourceFile = clean_path(args.EmbedSourceFile, "-EmbedSourceFile") type_map = scan_ref_types(args.SourceDir) register_columns = scan_register_columns(args.SourceDir) has_ref_types = len(type_map) > 0 + embed_requested = bool(args.EmbedSourceFile and args.EmbedSourceFile.strip()) + need_cfg = has_ref_types or embed_requested stub_format_version = detect_stub_format_version(args.SourceDir) stub_compat = stub_compatibility_mode(stub_format_version) ns_decl = f'{NS} version="{stub_format_version}"' @@ -1138,10 +1249,18 @@ def main(): if needs_registrator: type_map.setdefault('Document', {})['\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430'] = True # ЗаглушкаРегистратора - if has_ref_types: + if need_cfg: cfg_dir = os.path.join(temp_base, 'cfg') os.makedirs(cfg_dir, exist_ok=True) + embedded = None + if embed_requested: + embedded = add_source_object_to_config(args.EmbedSourceFile, cfg_dir) + if not embedded: + print('Error: %s is neither ExternalDataProcessor nor ExternalReport' % args.EmbedSourceFile, + file=sys.stderr) + sys.exit(1) + # Configuration.xml uuid_cfg = new_uuid() uuid_lang = new_uuid() @@ -1158,6 +1277,8 @@ def main(): tag = META_INFO[meta_type][0] for name in names: child_xml += f'\n\t\t\t<{tag}>{name}' + if embedded: + child_xml += '\n\t\t\t<%s>%s' % (embedded['tag'], embedded['name'], embedded['tag']) cfg_xml = f""" @@ -1388,7 +1509,7 @@ def main(): print(f'Creating infobase (ibcmd): {temp_base}') ib_data = tempfile.mkdtemp(prefix="stub_data_") ib_args = [args.V8Path, 'infobase', 'create', f'--db-path={temp_base}', '--create-database'] - if has_ref_types: + if need_cfg: ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force'] ib_args.append(f'--data={ib_data}') ib_args.extend(extra_args) @@ -1401,7 +1522,7 @@ def main(): print(result.stderr, file=sys.stderr) print(f'Failed to create stub infobase (code: {result.returncode})', file=sys.stderr) sys.exit(1) - if has_ref_types: + if need_cfg: import shutil shutil.rmtree(os.path.join(temp_base, 'cfg'), ignore_errors=True) print(f'[OK] Stub database created: {temp_base}') @@ -1417,13 +1538,24 @@ def main(): print(f'Failed to create infobase (code: {result.returncode})', file=sys.stderr) sys.exit(1) - if has_ref_types: + if need_cfg: cfg_dir = os.path.join(temp_base, 'cfg') # LoadConfigFromFiles print('Loading configuration from files...') + load_log = os.path.join(tempfile.gettempdir(), 'stub_load_log.txt') result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/LoadConfigFromFiles', f'"{cfg_dir}"', + '/Out', f'"{load_log}"', '/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args]) if result.returncode != 0: + # Причина отказа живёт только в /Out: в консоль пакетный 1cv8 не пишет ничего. + if os.path.isfile(load_log): + try: + with io.open(load_log, encoding='utf-8-sig', errors='replace') as fh: + text = fh.read().strip() + if text: + print(text) + except Exception: + pass print_platform_output(result) print(f'Failed to load config (code: {result.returncode})', file=sys.stderr) sys.exit(1) diff --git a/.claude/skills/epf-validate/SKILL.md b/.claude/skills/epf-validate/SKILL.md index 234407be2..b4f6468d4 100644 --- a/.claude/skills/epf-validate/SKILL.md +++ b/.claude/skills/epf-validate/SKILL.md @@ -12,6 +12,8 @@ allowed-tools: Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF). +Проверяется XML. Синтаксис модулей проверяет сборка: `/epf-build`, `/erf-build`. + ## Параметры | Параметр | Обяз. | Умолч. | Описание | diff --git a/.claude/skills/erf-build/SKILL.md b/.claude/skills/erf-build/SKILL.md index 1780f420f..2fbf0f859 100644 --- a/.claude/skills/erf-build/SKILL.md +++ b/.claude/skills/erf-build/SKILL.md @@ -25,7 +25,8 @@ allowed-tools: ## Параметры подключения (опционально) -Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы. +Предпочтительно использовать конкретную базу — это надёжнее. Временная база всё равно поднимается +под проверку исходников, если она не отключена. 1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую @@ -57,11 +58,22 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu | `-Password <пароль>` | нет | Пароль | | `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | | `-OutputFile <путь>` | да | Путь к выходному ERF-файлу | +| `-Checks <список>` | нет | Что проверить перед сборкой: `modules`, `handlers`, `unreferenced`, `empty-handlers`, `config`; `off` — не проверять. По умолчанию `modules,handlers` | +| `-Context <список>` | нет | Контексты проверки `modules`: `ThinClient`, `Server`, `ExternalConnection`, `ThickClientOrdinaryApplication`. По умолчанию `ThinClient,Server` | | `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` | | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` | > `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных +## Проверка перед сборкой + +Перед сборкой исходники проверяет платформа — синтаксис модулей и наличие обработчиков форм. +Если она нашла проблемы, сборка отменяется и файл не создаётся; в выводе — сообщение +платформы со строкой и колонкой и путь к файлу исходника. Отключается `-Checks off` +или ключом `"externalCheck": false` в `.v8-project.json`. + +Проверка идёт на отдельной временной базе, даже если база указана: чужую конфигурацию навык не трогает. + ## Примеры ```powershell diff --git a/.claude/skills/erf-validate/SKILL.md b/.claude/skills/erf-validate/SKILL.md index 355c42988..a7ecb9e44 100644 --- a/.claude/skills/erf-validate/SKILL.md +++ b/.claude/skills/erf-validate/SKILL.md @@ -14,6 +14,8 @@ allowed-tools: Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport). +Проверяется XML. Синтаксис модулей проверяет сборка: `/epf-build`, `/erf-build`. + ## Параметры | Параметр | Обяз. | Умолч. | Описание | diff --git a/docs/v8-project-guide.md b/docs/v8-project-guide.md index 34cf52b40..60d51c493 100644 --- a/docs/v8-project-guide.md +++ b/docs/v8-project-guide.md @@ -82,6 +82,7 @@ | `editingAllowedCheck` | `"deny"`/`"warn"`/`"off"` | нет | `deny` | Глобальная реакция support-guard на правку объектов на замке (см. ниже) | Руками | | `newObjectPosition` | `"end"`/`"byName"` | нет | `end` | Куда навыки ставят новый объект в `` (см. ниже) | Руками | | `extensionApplyCheck` | bool | нет | `true` | Проверять ли применимость расширения после загрузки в базу (навыки `db-load-*`, `db-update`); разово отключается ключом `-NoApplyCheck` | Руками | +| `externalCheck` | bool | нет | `true` | Проверять ли исходники платформой перед сборкой внешней обработки/отчёта (навыки `epf-build`, `erf-build`); разово отключается ключом `-Checks off` | Руками | | `skillSuggester` | `"on"`/`"off"` | нет | `on` | Подсказки навыков от хука skill-suggester (только если хук включён, см. ниже) | Руками | | `webPath` | string | нет | `tools/apache24` | Каталог Apache HTTP Server | Руками | | `ffmpegPath` | string | нет | `tools/ffmpeg/bin/ffmpeg.exe` | Путь к ffmpeg | Руками | diff --git a/tests/skills/cases/epf-build/check-command.json b/tests/skills/cases/epf-build/check-command.json new file mode 100644 index 000000000..7499d7ef3 --- /dev/null +++ b/tests/skills/cases/epf-build/check-command.json @@ -0,0 +1,39 @@ +{ + "name": "Перед сборкой запускается /CheckConfig (fake platform)", + "osOnly": "win32", + "fakePlatform": { + "exit": 0, + "log": "Загрузка завершена.\n", + "check": { + "exit": 0, + "log": "" + } + }, + "preRun": [ + { + "script": "epf-init/scripts/init", + "args": { + "-Name": "Тест", + "-SrcDir": "{workDir}/src" + } + } + ], + "args_extra": [ + "-V8Path", + "{workDir}\\fake.cmd", + "-InfoBasePath", + "{workDir}\\ib", + "-SourceFile", + "{workDir}\\src\\Тест.xml", + "-OutputFile", + "{workDir}\\build\\Тест.epf" + ], + "expect": { + "stdoutContains": [ + "/CheckConfig -ThinClient -Server -HandlersExistence", + "/LoadExternalDataProcessorOrReportFromFiles" + ] + }, + "expectError": true, + "noSnapshot": "платформа фейковая, EPF не собирается — проверяется командная строка проверки" +} diff --git a/tests/skills/cases/epf-build/check-findings-block-build.json b/tests/skills/cases/epf-build/check-findings-block-build.json new file mode 100644 index 000000000..4825b7421 --- /dev/null +++ b/tests/skills/cases/epf-build/check-findings-block-build.json @@ -0,0 +1,43 @@ +{ + "name": "Находки проверки отменяют сборку (fake platform)", + "osOnly": "win32", + "fakePlatform": { + "exit": 0, + "log": "", + "check": { + "exit": 101, + "log": "{Обработка.Тест.МодульОбъекта(5,1)}: Ожидается ключевое слово 'КонецФункции'\r\n" + } + }, + "preRun": [ + { + "script": "epf-init/scripts/init", + "args": { + "-Name": "Тест", + "-SrcDir": "{workDir}/src" + } + } + ], + "args_extra": [ + "-V8Path", + "{workDir}\\fake.cmd", + "-InfoBasePath", + "{workDir}\\ib", + "-SourceFile", + "{workDir}\\src\\Тест.xml", + "-OutputFile", + "{workDir}\\build\\Тест.epf" + ], + "expect": { + "stdoutContains": [ + "платформа нашла проблемы в исходниках", + "МодульОбъекта(5,1)", + "src\\Тест\\Ext\\ObjectModule.bsl" + ], + "stdoutNotContains": [ + "/LoadExternalDataProcessorOrReportFromFiles" + ] + }, + "expectError": true, + "noSnapshot": "сборка отменена — артефакта нет по замыслу" +} diff --git a/tests/skills/cases/epf-build/check-ibcmd-args-not-leaked.json b/tests/skills/cases/epf-build/check-ibcmd-args-not-leaked.json new file mode 100644 index 000000000..56f722515 --- /dev/null +++ b/tests/skills/cases/epf-build/check-ibcmd-args-not-leaked.json @@ -0,0 +1,56 @@ +{ + "name": "ibcmd-аргументы не утекают в проверку (fake platform)", + "osOnly": "win32", + "preRun": [ + { + "script": "epf-init/scripts/init", + "args": { + "-Name": "Тест", + "-SrcDir": "{workDir}/src" + } + }, + { + "writeFile": { + "path": "ibcmd.cmd", + "content": "@echo off\r\necho ARGS: %*\r\nexit /b 0\r\n", + "executable": true + } + }, + { + "writeFile": { + "path": "1cv8.cmd", + "content": "@echo off\r\necho ARGS: %*\r\nexit /b 0\r\n", + "executable": true + } + }, + { + "writeFile": { + "path": "ib/1Cv8.1CD", + "content": "stub\n" + } + } + ], + "args_extra": [ + "-V8Path", + "{workDir}\\ibcmd.cmd", + "-InfoBasePath", + "{workDir}\\ib", + "-SourceFile", + "{workDir}\\src\\Тест.xml", + "-OutputFile", + "{workDir}\\build\\Тест.epf", + "-AdditionalIbcmdArguments", + "--nonesuch=1" + ], + "expect": { + "stdoutContains": [ + "/CheckConfig -ThinClient -Server -HandlersExistence", + "Running: ibcmd infobase config import" + ], + "stdoutNotContains": [ + "/DisableStartupDialogs --nonesuch=1" + ] + }, + "expectError": true, + "noSnapshot": "платформа фейковая, EPF не собирается — проверяется состав командных строк" +} diff --git a/tests/skills/cases/epf-build/check-off.json b/tests/skills/cases/epf-build/check-off.json new file mode 100644 index 000000000..b51a3f75b --- /dev/null +++ b/tests/skills/cases/epf-build/check-off.json @@ -0,0 +1,43 @@ +{ + "name": "-Checks off отключает проверку (fake platform)", + "osOnly": "win32", + "fakePlatform": { + "exit": 0, + "log": "", + "check": { + "exit": 101, + "log": "Не должно быть вызвано\n" + } + }, + "preRun": [ + { + "script": "epf-init/scripts/init", + "args": { + "-Name": "Тест", + "-SrcDir": "{workDir}/src" + } + } + ], + "args_extra": [ + "-V8Path", + "{workDir}\\fake.cmd", + "-InfoBasePath", + "{workDir}\\ib", + "-SourceFile", + "{workDir}\\src\\Тест.xml", + "-OutputFile", + "{workDir}\\build\\Тест.epf", + "-Checks", + "off" + ], + "expect": { + "stdoutContains": [ + "/LoadExternalDataProcessorOrReportFromFiles" + ], + "stdoutNotContains": [ + "/CheckConfig" + ] + }, + "expectError": true, + "noSnapshot": "платформа фейковая, EPF не собирается — проверяется командная строка" +} diff --git a/tests/skills/cases/epf-build/check-path-only-if-exists.json b/tests/skills/cases/epf-build/check-path-only-if-exists.json new file mode 100644 index 000000000..4c250d262 --- /dev/null +++ b/tests/skills/cases/epf-build/check-path-only-if-exists.json @@ -0,0 +1,42 @@ +{ + "name": "Путь не печатается, если файла нет (fake platform)", + "osOnly": "win32", + "fakePlatform": { + "exit": 0, + "log": "", + "check": { + "exit": 101, + "log": "{Обработка.Тест.МодульМенеджера(3,1)}: Ошибка\r\n" + } + }, + "preRun": [ + { + "script": "epf-init/scripts/init", + "args": { + "-Name": "Тест", + "-SrcDir": "{workDir}/src" + } + } + ], + "args_extra": [ + "-V8Path", + "{workDir}\\fake.cmd", + "-InfoBasePath", + "{workDir}\\ib", + "-SourceFile", + "{workDir}\\src\\Тест.xml", + "-OutputFile", + "{workDir}\\build\\Тест.epf" + ], + "expect": { + "stdoutContains": [ + "платформа нашла проблемы в исходниках", + "МодульМенеджера(3,1)" + ], + "stdoutNotContains": [ + "ManagerModule.bsl" + ] + }, + "expectError": true, + "noSnapshot": "сборка отменена — артефакта нет по замыслу" +} diff --git a/tests/skills/cases/epf-build/check-skipped-no-1cv8.json b/tests/skills/cases/epf-build/check-skipped-no-1cv8.json new file mode 100644 index 000000000..c13f8da6c --- /dev/null +++ b/tests/skills/cases/epf-build/check-skipped-no-1cv8.json @@ -0,0 +1,47 @@ +{ + "name": "ibcmd без соседнего 1cv8: проверка пропускается, сборка идёт (fake platform)", + "osOnly": "win32", + "preRun": [ + { + "script": "epf-init/scripts/init", + "args": { + "-Name": "Тест", + "-SrcDir": "{workDir}/src" + } + }, + { + "writeFile": { + "path": "ibcmd.cmd", + "content": "@echo off\r\necho ARGS: %*\r\nexit /b 0\r\n", + "executable": true + } + }, + { + "writeFile": { + "path": "ib/1Cv8.1CD", + "content": "stub\n" + } + } + ], + "args_extra": [ + "-V8Path", + "{workDir}\\ibcmd.cmd", + "-InfoBasePath", + "{workDir}\\ib", + "-SourceFile", + "{workDir}\\src\\Тест.xml", + "-OutputFile", + "{workDir}\\build\\Тест.epf" + ], + "expect": { + "stdoutContains": [ + "[note] source check skipped", + "Running: ibcmd infobase config import" + ], + "stdoutNotContains": [ + "/CheckConfig" + ] + }, + "expectError": true, + "noSnapshot": "платформа фейковая, EPF не собирается — проверяется поведение без 1cv8 рядом" +} diff --git a/tests/skills/cases/epf-build/error-context-without-modules.json b/tests/skills/cases/epf-build/error-context-without-modules.json new file mode 100644 index 000000000..73bee57a4 --- /dev/null +++ b/tests/skills/cases/epf-build/error-context-without-modules.json @@ -0,0 +1,42 @@ +{ + "name": "-Context без modules отбивается", + "osOnly": "win32", + "fakePlatform": { + "exit": 0, + "log": "" + }, + "preRun": [ + { + "script": "epf-init/scripts/init", + "args": { + "-Name": "Тест", + "-SrcDir": "{workDir}/src" + } + } + ], + "args_extra": [ + "-V8Path", + "{workDir}\\fake.cmd", + "-InfoBasePath", + "{workDir}\\ib", + "-SourceFile", + "{workDir}\\src\\Тест.xml", + "-OutputFile", + "{workDir}\\build\\Тест.epf", + "-Checks", + "handlers", + "-Context", + "Server" + ], + "expect": { + "stdoutContains": [ + "-Context задан, но в -Checks нет modules" + ], + "stdoutNotContains": [ + "/CheckConfig", + "/LoadExternalDataProcessorOrReportFromFiles" + ] + }, + "expectError": true, + "noSnapshot": "отказ до запуска платформы" +} diff --git a/tests/skills/cases/epf-build/error-source-rejected-at-prepare.json b/tests/skills/cases/epf-build/error-source-rejected-at-prepare.json new file mode 100644 index 000000000..56dda2489 --- /dev/null +++ b/tests/skills/cases/epf-build/error-source-rejected-at-prepare.json @@ -0,0 +1,36 @@ +{ + "name": "Отказ платформы при подготовке проверки не выдаётся за сбой базы (fake platform)", + "osOnly": "win32", + "fakePlatform": { + "exit": 1, + "log": "" + }, + "preRun": [ + { + "script": "epf-init/scripts/init", + "args": { + "-Name": "Тест", + "-SrcDir": "{workDir}/src" + } + } + ], + "args_extra": [ + "-V8Path", + "{workDir}\\fake.cmd", + "-SourceFile", + "{workDir}\\src\\Тест.xml", + "-OutputFile", + "{workDir}\\build\\Тест.epf" + ], + "expect": { + "stdoutContains": [ + "платформа не приняла исходники" + ], + "stdoutNotContains": [ + "failed to create stub database", + "/LoadExternalDataProcessorOrReportFromFiles" + ] + }, + "expectError": true, + "noSnapshot": "платформа фейковая и падает на первом запуске — проверяется вердикт" +} diff --git a/tests/skills/cases/epf-build/error-unknown-check.json b/tests/skills/cases/epf-build/error-unknown-check.json new file mode 100644 index 000000000..12faea129 --- /dev/null +++ b/tests/skills/cases/epf-build/error-unknown-check.json @@ -0,0 +1,40 @@ +{ + "name": "Неизвестная проверка отбивается", + "osOnly": "win32", + "fakePlatform": { + "exit": 0, + "log": "" + }, + "preRun": [ + { + "script": "epf-init/scripts/init", + "args": { + "-Name": "Тест", + "-SrcDir": "{workDir}/src" + } + } + ], + "args_extra": [ + "-V8Path", + "{workDir}\\fake.cmd", + "-InfoBasePath", + "{workDir}\\ib", + "-SourceFile", + "{workDir}\\src\\Тест.xml", + "-OutputFile", + "{workDir}\\build\\Тест.epf", + "-Checks", + "syntax" + ], + "expect": { + "stdoutContains": [ + "unknown check 'syntax'" + ], + "stdoutNotContains": [ + "/CheckConfig", + "/LoadExternalDataProcessorOrReportFromFiles" + ] + }, + "expectError": true, + "noSnapshot": "отказ до запуска платформы" +} diff --git a/tests/skills/cases/epf-build/extra-args-build.json b/tests/skills/cases/epf-build/extra-args-build.json index 11b0e07dd..e796af72a 100644 --- a/tests/skills/cases/epf-build/extra-args-build.json +++ b/tests/skills/cases/epf-build/extra-args-build.json @@ -2,6 +2,13 @@ "name": "Доп. аргументы доходят до самой сборки EPF (fake platform)", "osOnly": "win32", "preRun": [ + { + "script": "epf-init/scripts/init", + "args": { + "-Name": "Тест", + "-SrcDir": "{workDir}/src" + } + }, { "writeFile": { "path": "fake.cmd", @@ -13,12 +20,6 @@ "path": "ib/1Cv8.1CD", "content": "fake infobase\n" } - }, - { - "writeFile": { - "path": "src/Обработка.xml", - "content": "\n\n" - } } ], "args_extra": [ @@ -27,9 +28,9 @@ "-InfoBasePath", "{workDir}\\ib", "-SourceFile", - "{workDir}\\src\\Обработка.xml", + "{workDir}\\src\\Тест.xml", "-OutputFile", - "{workDir}\\build\\Обработка.epf", + "{workDir}\\build\\Тест.epf", "-AdditionalV8Arguments", "/UseHwLicenses+" ], diff --git a/tests/skills/cases/epf-build/extra-args-stub-chain.json b/tests/skills/cases/epf-build/extra-args-stub-chain.json index e880be323..d02d8eef2 100644 --- a/tests/skills/cases/epf-build/extra-args-stub-chain.json +++ b/tests/skills/cases/epf-build/extra-args-stub-chain.json @@ -3,15 +3,16 @@ "osOnly": "win32", "preRun": [ { - "writeFile": { - "path": "fake.cmd", - "content": "@echo off\r\necho ARGS: %*\r\nexit /b 1\r\n" + "script": "epf-init/scripts/init", + "args": { + "-Name": "Тест", + "-SrcDir": "{workDir}/src" } }, { "writeFile": { - "path": "src/Обработка.xml", - "content": "\n\n" + "path": "fake.cmd", + "content": "@echo off\r\necho ARGS: %*\r\nexit /b 1\r\n" } } ], @@ -19,9 +20,9 @@ "-V8Path", "{workDir}\\fake.cmd", "-SourceFile", - "{workDir}\\src\\Обработка.xml", + "{workDir}\\src\\Тест.xml", "-OutputFile", - "{workDir}\\build\\Обработка.epf", + "{workDir}\\build\\Тест.epf", "-AdditionalV8Arguments", "/UseHwLicenses+" ], diff --git a/tests/skills/check-inline-drift.mjs b/tests/skills/check-inline-drift.mjs index 23ae7e9e6..cc4d5f97a 100644 --- a/tests/skills/check-inline-drift.mjs +++ b/tests/skills/check-inline-drift.mjs @@ -72,7 +72,7 @@ const FAMILIES = [ // хранилища — задача одна, поэтому семья общая, а не вторая с тем же телом. { id: 'full', authority: 'cf-edit', consumers: ['cfe-borrow', 'db-cfe-admin', 'db-dump-xml', 'db-load-cf', 'db-load-git', 'db-load-xml', 'db-repo', 'db-update', - 'form-add', 'form-compile', 'form-edit', 'help-add', 'interface-edit', 'meta-compile', + 'epf-build', 'form-add', 'form-compile', 'form-edit', 'help-add', 'interface-edit', 'meta-compile', 'meta-edit', 'meta-remove', 'mxl-compile', 'role-compile', 'skd-compile', 'skd-edit', 'subsystem-compile', 'subsystem-edit', 'template-add', 'xdto-compile', 'xdto-edit'] }, ], diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs index f2de641d6..c566300d3 100644 --- a/tests/skills/runner.mjs +++ b/tests/skills/runner.mjs @@ -376,13 +376,14 @@ function buildArgs(skillConfig, caseData, workDir, inputFilePath, runtime) { // Второй ответ — на проверку применимости расширения: навык запускает её ОТДЕЛЬНЫМ процессом // (в одной командной строке платформа выполнила бы только последнюю команду), поэтому фейк // отличает проверку по составу аргументов, а не по номеру вызова. -const FAKE_PLATFORM_CMD = "@echo off\r\nrem SELF запоминаем ДО цикла: shift сдвигает и %0, после него %~dp0 указывает не на скрипт\r\nset SELF=%~dp0\r\nset KIND=main\r\n:loop\r\nif \"%~1\"==\"\" goto done\r\nif /i \"%~1\"==\"/Out\" set OUT=%~2\r\nif /i \"%~1\"==\"/CheckCanApplyConfigurationExtensions\" set KIND=check\r\nshift\r\ngoto loop\r\n:done\r\nif \"%KIND%\"==\"check\" if exist \"%SELF%log_check.txt\" (copy /y \"%SELF%log_check.txt\" \"%OUT%\" >nul & exit /b CHECKCODE)\r\ncopy /y \"%SELF%log.txt\" \"%OUT%\" >nul\r\nexit /b EXITCODE\r\n"; -const FAKE_PLATFORM_SH = "#!/bin/sh\n# Фейк платформы для *nix: вычитывает путь из /Out и кладёт туда готовый лог.\n# Значение /Out несёт кавычки ВНУТРИ токена (соглашение 1С, см. run_v8) — в batch их\n# снимает %~2, в sh их надо снять руками, иначе cp целится в имя с кавычками.\nSELF=$(dirname \"$0\")\nOUT=\"\"\nKIND=main\nwhile [ $# -gt 0 ]; do\n if [ \"$1\" = \"/CheckCanApplyConfigurationExtensions\" ]; then\n KIND=check\n fi\n if [ \"$1\" = \"/Out\" ]; then\n OUT=\"$2\"\n OUT=\"${OUT#\\\"}\"\n OUT=\"${OUT%\\\"}\"\n fi\n shift\ndone\nif [ \"$KIND\" = check ] && [ -f \"$SELF/log_check.txt\" ]; then\n cp \"$SELF/log_check.txt\" \"$OUT\"\n exit CHECKCODE\nfi\ncp \"$SELF/log.txt\" \"$OUT\"\nexit EXITCODE\n"; +const FAKE_PLATFORM_CMD = "@echo off\r\nrem SELF запоминаем ДО цикла: shift сдвигает и %0, после него %~dp0 указывает не на скрипт\r\nset SELF=%~dp0\r\nset KIND=main\r\n:loop\r\nif \"%~1\"==\"\" goto done\r\nif /i \"%~1\"==\"/Out\" set OUT=%~2\r\nif /i \"%~1\"==\"/CheckCanApplyConfigurationExtensions\" set KIND=check\r\nif /i \"%~1\"==\"/CheckConfig\" set KIND=check\r\nshift\r\ngoto loop\r\n:done\r\nif \"%KIND%\"==\"check\" if exist \"%SELF%log_check.txt\" (copy /y \"%SELF%log_check.txt\" \"%OUT%\" >nul & exit /b CHECKCODE)\r\ncopy /y \"%SELF%log.txt\" \"%OUT%\" >nul\r\nexit /b EXITCODE\r\n"; +const FAKE_PLATFORM_SH = "#!/bin/sh\n# Фейк платформы для *nix: вычитывает путь из /Out и кладёт туда готовый лог.\n# Значение /Out несёт кавычки ВНУТРИ токена (соглашение 1С, см. run_v8) — в batch их\n# снимает %~2, в sh их надо снять руками, иначе cp целится в имя с кавычками.\nSELF=$(dirname \"$0\")\nOUT=\"\"\nKIND=main\nwhile [ $# -gt 0 ]; do\n if [ \"$1\" = \"/CheckCanApplyConfigurationExtensions\" ] || [ \"$1\" = \"/CheckConfig\" ]; then\n KIND=check\n fi\n if [ \"$1\" = \"/Out\" ]; then\n OUT=\"$2\"\n OUT=\"${OUT#\\\"}\"\n OUT=\"${OUT%\\\"}\"\n fi\n shift\ndone\nif [ \"$KIND\" = check ] && [ -f \"$SELF/log_check.txt\" ]; then\n cp \"$SELF/log_check.txt\" \"$OUT\"\n exit CHECKCODE\nfi\ncp \"$SELF/log.txt\" \"$OUT\"\nexit EXITCODE\n"; function writeFakePlatform(workDir, spec) { const isWin = process.platform === 'win32'; const code = Number.isInteger(spec.exit) ? spec.exit : 0; - // spec.check — ответ на проверку применимости расширения (отдельный запуск платформы) + // spec.check — ответ платформы на отдельный запуск проверки + // (/CheckCanApplyConfigurationExtensions у расширений, /CheckConfig у внешних обработок) const checkCode = spec.check && Number.isInteger(spec.check.exit) ? spec.check.exit : 0; const body = (isWin ? FAKE_PLATFORM_CMD : FAKE_PLATFORM_SH) .replaceAll('EXITCODE', String(code))