feat(epf-build): исходники проверяются платформой до сборки

Сборка .epf/.erf не компилирует модули, поэтому сломанный BSL уезжал
пользователю и падал при открытии обработки. Прямой команды «проверь
внешнюю обработку» у платформы нет, но объект конфигурации она проверяет:
обработка кладётся в конфигурацию временной базы (stub-db-create
-EmbedSourceFile) и спрашивается /CheckConfig. При находках сборка
отменяется, в выводе — сообщение платформы и путь к файлу исходника.

Ключи -Checks и -Context; выключение -Checks off или externalCheck: false
в .v8-project.json. С выключенной проверкой поведение прежнее.

У копии объекта перевыпускаются все GUID: с идентификаторами исходника
платформа путает копию в базе с загружаемой внешней обработкой и через раз
отвечает «Исключение XDTO при чтении файла» на исправном исходнике.

Ручная матрица (база есть/нет × ps1/py × исправный/битый × с модулями/без)
вскрыла ещё три вещи: отказ платформы принять исходники при подготовке
проверки выдавался за сбой базы; py-порт стаба терял причину отказа
LoadConfigFromFiles (не было /Out, в PS он есть); копии общих утилит в
stub-db-create.py разошлись с эталоном db-create — у run_v8 не было
POSIX-ветки, из-за чего на darwin/Linux временная база с пробелом в пути
молча не находилась бы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MQkqXxErBepYmoUycfkbn
This commit is contained in:
Nick Shirokov
2026-09-05 17:46:30 +03:00
co-authored by Claude Opus 5
parent a3ea0b7be6
commit 448f05ed11
23 changed files with 1081 additions and 67 deletions
+1
View File
@@ -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 базы по умолчанию |
+13 -1
View File
@@ -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
+192 -16
View File
@@ -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/<ver>/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
}
}
+197 -13
View File
@@ -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__":
@@ -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 '<ExternalDataProcessor[\s>]') {
$extTag = 'ExternalDataProcessor'; $cfgTag = 'DataProcessor'; $folder = 'DataProcessors'
} elseif ($text -match '<ExternalReport[\s>]') {
$extTag = 'ExternalReport'; $cfgTag = 'Report'; $folder = 'Reports'
} else {
return $null
}
$name = if ($text -match '<Name>([^<]+)</Name>') { $Matches[1] } else { [IO.Path]::GetFileNameWithoutExtension($SourceXml) }
$conv = $reGuid.Replace($text, $reissue)
$conv = $conv.Replace("<$extTag ", "<$cfgTag ").Replace("<$extTag>", "<$cfgTag>").Replace("</$extTag>", "</$cfgTag>")
$conv = $conv.Replace("${extTag}Object.", "${cfgTag}Object.").Replace("$extTag.", "$cfgTag.")
# Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен:
# без него платформа отвечает «отсутствует один или более типов объекта».
$mgr = "`t`t`t<xr:GeneratedType name=`"${cfgTag}Manager.$name`" category=`"Manager`">`r`n" +
"`t`t`t`t<xr:TypeId>$([guid]::NewGuid().ToString())</xr:TypeId>`r`n" +
"`t`t`t`t<xr:ValueId>$([guid]::NewGuid().ToString())</xr:ValueId>`r`n" +
"`t`t`t</xr:GeneratedType>`r`n"
if ($conv -match '</InternalInfo>') {
$conv = [regex]::Replace($conv, '(\s*)</InternalInfo>', ("`r`n" + $mgr + "`t`t</InternalInfo>"), 1)
} else {
$objType = "`t`t`t<xr:GeneratedType name=`"${cfgTag}Object.$name`" category=`"Object`">`r`n" +
"`t`t`t`t<xr:TypeId>$([guid]::NewGuid().ToString())</xr:TypeId>`r`n" +
"`t`t`t`t<xr:ValueId>$([guid]::NewGuid().ToString())</xr:ValueId>`r`n" +
"`t`t`t</xr:GeneratedType>`r`n"
$conv = [regex]::Replace($conv, "(<$cfgTag[^>]*>)", ("`$1`r`n`t`t<InternalInfo>`r`n" + $objType + $mgr + "`t`t</InternalInfo>"), 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</$tag>"
}
}
if ($embedded) { $childXml += "`r`n`t`t`t<$($embedded.Tag)>$($embedded.Name)</$($embedded.Tag)>" }
$cfgXml = @"
<?xml version="1.0" encoding="UTF-8"?>
@@ -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..."
@@ -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'<ExternalDataProcessor[\s>]', text):
ext_tag, cfg_tag, folder = 'ExternalDataProcessor', 'DataProcessor', 'DataProcessors'
elif re.search(r'<ExternalReport[\s>]', text):
ext_tag, cfg_tag, folder = 'ExternalReport', 'Report', 'Reports'
else:
return None
m = re.search(r'<Name>([^<]+)</Name>', 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('</%s>' % ext_tag, '</%s>' % cfg_tag)
conv = conv.replace('%sObject.' % ext_tag, '%sObject.' % cfg_tag).replace('%s.' % ext_tag, '%s.' % cfg_tag)
# Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен:
# без него платформа отвечает «отсутствует один или более типов объекта».
mgr = ('\t\t\t<xr:GeneratedType name="%sManager.%s" category="Manager">\r\n' % (cfg_tag, name) +
'\t\t\t\t<xr:TypeId>%s</xr:TypeId>\r\n' % new_uuid() +
'\t\t\t\t<xr:ValueId>%s</xr:ValueId>\r\n' % new_uuid() +
'\t\t\t</xr:GeneratedType>\r\n')
if '</InternalInfo>' in conv:
conv = re.sub(r'\s*</InternalInfo>', lambda _m: '\r\n' + mgr + '\t\t</InternalInfo>', conv, count=1)
else:
obj_type = ('\t\t\t<xr:GeneratedType name="%sObject.%s" category="Object">\r\n' % (cfg_tag, name) +
'\t\t\t\t<xr:TypeId>%s</xr:TypeId>\r\n' % new_uuid() +
'\t\t\t\t<xr:ValueId>%s</xr:ValueId>\r\n' % new_uuid() +
'\t\t\t</xr:GeneratedType>\r\n')
conv = re.sub('(<%s[^>]*>)' % cfg_tag,
lambda m2: m2.group(1) + '\r\n\t\t<InternalInfo>\r\n' + obj_type + mgr + '\t\t</InternalInfo>',
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}</{tag}>'
if embedded:
child_xml += '\n\t\t\t<%s>%s</%s>' % (embedded['tag'], embedded['name'], embedded['tag'])
cfg_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject {ns_decl}>
@@ -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)
+2
View File
@@ -12,6 +12,8 @@ allowed-tools:
Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF).
Проверяется XML. Синтаксис модулей проверяет сборка: `/epf-build`, `/erf-build`.
## Параметры
| Параметр | Обяз. | Умолч. | Описание |
+13 -1
View File
@@ -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
+2
View File
@@ -14,6 +14,8 @@ allowed-tools:
Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport).
Проверяется XML. Синтаксис модулей проверяет сборка: `/epf-build`, `/erf-build`.
## Параметры
| Параметр | Обяз. | Умолч. | Описание |
+1
View File
@@ -82,6 +82,7 @@
| `editingAllowedCheck` | `"deny"`/`"warn"`/`"off"` | нет | `deny` | Глобальная реакция support-guard на правку объектов на замке (см. ниже) | Руками |
| `newObjectPosition` | `"end"`/`"byName"` | нет | `end` | Куда навыки ставят новый объект в `<ChildObjects>` (см. ниже) | Руками |
| `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 | Руками |
@@ -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 не собирается — проверяется командная строка проверки"
}
@@ -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": "сборка отменена — артефакта нет по замыслу"
}
@@ -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 не собирается — проверяется состав командных строк"
}
@@ -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 не собирается — проверяется командная строка"
}
@@ -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": "сборка отменена — артефакта нет по замыслу"
}
@@ -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 рядом"
}
@@ -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": "отказ до запуска платформы"
}
@@ -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": "платформа фейковая и падает на первом запуске — проверяется вердикт"
}
@@ -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": "отказ до запуска платформы"
}
@@ -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": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<MetaDataObject xmlns=\"http://v8.1c.ru/8.3/MDClasses\"/>\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+"
],
@@ -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": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<MetaDataObject xmlns=\"http://v8.1c.ru/8.3/MDClasses\"/>\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+"
],
+1 -1
View File
@@ -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'] },
],
+4 -3
View File
@@ -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))