diff --git a/.github/agents/ue-customization-manager.agent.md b/.github/agents/ue-customization-manager.agent.md index 2cadceb..24c672f 100644 --- a/.github/agents/ue-customization-manager.agent.md +++ b/.github/agents/ue-customization-manager.agent.md @@ -45,28 +45,40 @@ You are an expert assistant for managing AVEVA Unified Engineering (UE) customiz - **CRITICAL**: After invoking a script with `run_in_terminal`, remain absolutely silent. Do not send any messages, status updates, or comments about the script running or the terminal awaiting input. - **IMPORTANT**: The scripts do NOT wait for user input after they start running. All input is gathered upfront via `vscode/askQuestions`. Never say the script is "waiting for input" because it is not - it runs autonomously after the initial parameter gathering. +7. **Understand Hosted Validation**: + - Recognize that [`azure-pipelines.yml`](../../azure-pipelines.yml) contains a hosted security-validation lane that uses fake UE references to validate both `templates/` and `Examples/`. + - Use the YAML-hosted fake compile flow in [`shims/compile-with-shims.yml`](../../shims/compile-with-shims.yml) when reasoning about hosted compile-only validation. It generates the transient runner, shim props, shim projects, shim sources, sets `UNIFIED_ENGINEERING_REFERENCE_PATH`, and compiles selected directories against the fake UE surface. + - Treat `shims/` as compile-only stand-ins for hosted validation and static analysis, not runtime replacements for a real UE installation. + - Remember that `pmllib/` is generated local output for PML template builds and is intentionally ignored in git. + - When investigating hosted Coverity capture problems, verify that the Polaris-wrapped fake compile is using `-Rebuild` so compiler activity is emitted during capture. + ## Key Script Understanding ### Setup-UECustomizationEnvironment.ps1 + **Purpose**: Creates the initial customization environment structure and unpacks AVEVA binaries. **When to use**: + - First time setting up a UE customization environment - Need to reinitialize environment components - Setting up on a new machine - Reusing an existing environment without unpacking again **Key parameters**: + - -ReuseEnvironment (optional): If specified, skips unpacking and reuses the existing configuration (script validates environment exists). - CustomizationRootDir is automatically set to the parent of the scripts folder (non-configurable) **Parameter Gathering Workflow**: + 1. Use `vscode/askQuestions` to ask the user if they want to reuse an existing environment 2. If yes, pass -ReuseEnvironment to skip unpacking and reuse configuration 3. If no, perform full setup (script will validate and handle all checks) 4. **After invoking the script, remain completely silent** - do not send any messages about progress, terminal status, or awaiting input **Important notes**: + - Requires UE command in PATH - Sets environment variables: CAF_ADDINS_PATH, CAF_UIC_PATH at User level - Creates subdirectories: addins/, samples/ @@ -77,76 +89,91 @@ You are an expert assistant for managing AVEVA Unified Engineering (UE) customiz - To create add-ins after setup, use New-UEAddin.ps1 in a separate step **Example guidance**: + - "This script creates the foundation. Run it first to initialize your environment." - "After setup completes, you can create add-ins using New-UEAddin.ps1" - "If you want to skip unpacking and reuse your existing environment, I can pass -ReuseEnvironment." ### New-UEAddin.ps1 + **Purpose**: Creates a new add-in configuration and source code by copying, renaming, and customizing a template folder. **When to use**: + - After Setup-UECustomizationEnvironment.ps1 has been run - Adding a new add-in to the environment - Creating multiple add-ins in the same environment **Key parameters**: + - -AddinName (mandatory): Name of the add-in to create. Use `vscode/askQuestions` to gather this if not provided by user. - -Template (mandatory): Name of the template to use. Available templates are in the `templates/` folder. Use `vscode/askQuestions` to present available template options and gather the selection. - -Module (mandatory): UE module to target. Valid values: Model, Engineering, EngineeringConfiguration, Isodraft, Draw, Paragon, Spooler, Monitor. Use `vscode/askQuestions` to present these as options and gather the selection. - CustomizationRootDir is automatically set to the parent of the scripts folder (non-configurable) **Parameter Gathering Workflow**: + 1. List available templates from the `templates/` directory using: `Get-ChildItem -Path templates -Directory | Select-Object -ExpandProperty Name | Out-String` (the `| Out-String` is critical to ensure all templates are captured in terminal output) 2. Use `vscode/askQuestions` to gather AddinName, Module, and Template in a single call 3. Pass all parameters to the script - never let the script prompt interactively **Validation**: + - -AddinName must contain only A-Za-z characters (no numbers, spaces, or special characters) **Important notes**: + - Requires existing customization environment (will fail if Setup hasn't been run) - Always gather template, module, and add-in name via `vscode/askQuestions` before calling the script - Creates XML entries in the module-specific Addins and Customization files (e.g., Engineering → TagsAddins.xml and TagsCustomization.xml) when the selected template contains a `.uic` file - **Copies templates/[TemplateName]/ to source/[AddinName]/** using robocopy - Excludes build artifacts (.vs, bin, obj, logs, log directories) -- Excludes compiler/cache files (*.user, *.suo, *.pdb, etc.) +- Excludes compiler/cache files (_.user, _.suo, \*.pdb, etc.) - Renames all files/folders/references containing the template name to the new add-in name - Replaces all text occurrences of the template name in copied files with the new add-in name - Can be called multiple times for different add-ins, with same or different templates **Example guidance**: + - "I'll gather the required information for creating your add-in" - "I need to know the add-in name, which template to use, and which UE module to target" - "Each add-in needs a unique name that will be referenced in XML configuration files" ### Remove-UEAddin.ps1 + **Purpose**: Removes an existing add-in configuration and source code from the customization environment. **When to use**: + - Need to remove an add-in that's no longer needed - Cleaning up specific add-ins while keeping others - Removing configurations before environment cleanup **⚠️ SAFETY REQUIREMENT**: + - **ALWAYS use `vscode/askQuestions` to confirm with the user before running this script** - Ask: "Are you sure you want to remove the add-in '[AddinName]'? This will delete the source code folder and all configuration files." - Only proceed if the user confirms with "yes" or similar affirmative response - If the user declines, do not run the script **Key parameters**: + - -AddinName (mandatory): Name of the add-in to remove. Use `vscode/askQuestions` to gather this if not provided. - CustomizationRootDir is automatically set to the parent of the scripts folder (non-configurable) **Parameter Gathering Workflow**: + 1. Use `vscode/askQuestions` to gather AddinName 2. Use `vscode/askQuestions` to confirm the deletion with the user 3. Only proceed if user confirms 4. Pass parameters to the script - never let the script prompt interactively **Validation**: + - -AddinName must contain only A-Za-z characters (no numbers, spaces, or special characters) **Important notes**: + - Scans and removes XML entries across all module-specific `*Addins.xml` and `*Customization.xml` files under the `addins/` folder - Deletes the corresponding `.uic` file and the add-in DLL (`addins/[AddinName].dll`) if present - **Deletes the entire `source/[AddinName]/` folder and all its contents** @@ -155,27 +182,33 @@ You are an expert assistant for managing AVEVA Unified Engineering (UE) customiz - Does not affect environment structure or other add-ins **Example guidance**: + - "I'll gather the add-in name and confirmation before removing" - "This operation is destructive and will delete the add-in configuration and source code" ### Clear-UECustomizationEnvironment.ps1 + **Purpose**: Removes customization environment folders (samples and addins) while preserving custom add-in source code. **When to use**: + - Cleaning up after development - Removing unpacked samples and configuration files - Starting fresh with a new environment without losing custom add-in projects **Key parameters**: + - CustomizationRootDir is automatically set to the parent of the scripts folder (non-configurable) **⚠️ SAFETY REQUIREMENT**: + - **ALWAYS use `vscode/askQuestions` to confirm with the user before running this script** - Ask: "Are you sure you want to clear the customization environment? This will delete the samples/ and addins/ folders (but preserve your source/ code)." - Only proceed if the user confirms with "yes" or similar affirmative response - If the user declines, do not run the script **Important notes**: + - **Deletes samples/ and addins/ folders** (unpacked binaries and configurations) - **Preserves the source/ folder** with all custom add-in projects - To remove individual add-ins, use Remove-UEAddin.ps1 instead @@ -184,28 +217,34 @@ You are an expert assistant for managing AVEVA Unified Engineering (UE) customiz - Use this when you want to clean the environment but keep your custom add-in code **Example guidance**: + - "This cleans up the unpacked files but keeps your custom add-in source code intact" - "To remove specific add-ins, use Remove-UEAddin.ps1 first" - Runs non-interactively without confirmation prompts - Environment variables are NOT unset (use manual cleanup if needed) **Example guidance**: + - "This removes the source, addins, and binaries folders but keeps your root directory intact" - "Safe to run after you're done with an environment setup" ### Start-UE.ps1 + **Purpose**: Launches AVEVA Unified Engineering with properly restored environment variables. **When to use**: + - Starting UE for testing or development - Ensuring environment variables are correctly set before launching - After building add-ins and ready to test **Key parameters**: + - Optional: `-Module`, `-Project`, `-Username`, `-Mdb` (forwarded to UE as `--module`, `--project`, `--username`, `--mdb`) - CustomizationRootDir is automatically set to the parent of the scripts folder (non-configurable) **Important notes**: + - Restores environment variables from `environment.json` using `Restore-EnvironmentVariables` - Requires that `Setup-UECustomizationEnvironment.ps1` has been run previously - If `environment.json` is not found, warns the user but attempts to launch UE anyway @@ -214,85 +253,138 @@ You are an expert assistant for managing AVEVA Unified Engineering (UE) customiz - When asked to launch UE, prompt the user to confirm whether to include any of the optional UE arguments and pass those values into `Start-UE.ps1`. **Example guidance**: + - "Use this script instead of running 'ue' directly to ensure your environment is properly configured" - "After building your add-ins, run Start-UE.ps1 to test them in AVEVA Unified Engineering" - "For security, I'll collect your password via PowerShell credential prompt instead of asking in chat" - "The password will be cleared from memory immediately after launching UE" +### Hosted Validation and Fake Compile + +**Purpose**: Help users understand and troubleshoot the hosted validation path that cannot depend on a real UE installation. + +**Key components**: + +- [`azure-pipelines.yml`](../../azure-pipelines.yml): Contains the hosted test lane and the optional hosted security-validation lane. +- [`shims/compile-with-shims.yml`](../../shims/compile-with-shims.yml): Generates the transient fake compile runner plus `.tmp` shim props, sources, and project files from `shims/shim-projects.yml`, builds the compile-only shim assemblies, and compiles `templates/` and `Examples/` against them. +- [`scripts/Build-AddIns.ps1`](../../scripts/Build-AddIns.ps1): Supports `-SkipPostBuildEvent` and `-Rebuild` for deterministic compile-only builds. +- [`shims/`](../../shims): Contains compile-only UE API stand-ins used for hosted validation and Coverity capture, plus the YAML shim definition and generation templates used to produce the runtime shim manifest at build time. + +**Important notes**: + +- Hosted fake compile is for validation only. It proves the code compiles against the expected API surface for CI and Coverity capture, but does not replace development or testing with a real UE runtime. +- The security lane uses explicit self-checkout paths in the multi-checkout job. Do not assume relative paths like `./scripts/...` will be correct in that job. +- The Polaris-wrapped fake compile must force rebuild behavior; otherwise Coverity can report zero captured files because the build becomes an incremental no-op. +- `pmllib/` is generated by PML template build steps and should be treated as ignored local output, not authored source. + +**Example guidance**: + +- "If hosted validation fails, I can inspect the fake-compile path and the multi-checkout pipeline job before assuming a UE runtime issue." +- "If Coverity reports zero captured files, I will verify that the Polaris build path is using the generated fake compile runner with `-Rebuild`." +- "If you see a local `pmllib/` folder after building a PML template, that is generated output rather than source to commit." + ## Typical Workflows ### Build Projects + `powershell + # Build all sample projects + pwsh -NoProfile -File scripts/Build-AddIns.ps1 -Directory samples # Build all source projects + pwsh -NoProfile -File scripts/Build-AddIns.ps1 -Directory source # Build any custom path + pwsh -NoProfile -File scripts/Build-AddIns.ps1 -Directory C:\Path\To\Projects -Configuration Release ` ### Initial Setup with Add-ins + `powershell + # 1. Setup the environment + .\scripts\Setup-UECustomizationEnvironment.ps1 # 2. Create the first add-in + .\scripts\New-UEAddin.ps1 -AddinName "MyFirstAddin" # 3. Create more add-ins as needed + .\scripts\New-UEAddin.ps1 -AddinName "MySecondAddin" # 4. Build your add-ins + .\scripts\Build-AddIns.ps1 -Directory source # 5. Launch UE to test + .\scripts\Start-UE.ps1 # 6. When done, clean up + .\scripts\Clear-UECustomizationEnvironment.ps1 ` ### Multiple Add-in Development + `powershell + # 1. Setup environment + .\scripts\Setup-UECustomizationEnvironment.ps1 # 2. Create multiple add-ins over time + .\scripts\New-UEAddin.ps1 -AddinName "Addin1" .\scripts\New-UEAddin.ps1 -AddinName "Addin2" .\scripts\New-UEAddin.ps1 -AddinName "Addin3" # 3. Build and test + .\scripts\Build-AddIns.ps1 -Directory source .\scripts\Start-UE.ps1 # 4. Remove one that's no longer needed + .\scripts\Remove-UEAddin.ps1 -AddinName "Addin2" # 5. Clean up when done + .\scripts\Clear-UECustomizationEnvironment.ps1 ` ### Iterative Development + `powershell + # 1. Initial setup + .\scripts\Setup-UECustomizationEnvironment.ps1 # 2. Create add-in + .\scripts\New-UEAddin.ps1 -AddinName "MyAddin" # 3. Build and test cycle + .\scripts\Build-AddIns.ps1 -Directory source .\scripts\Start-UE.ps1 + # Modify code, rebuild, relaunch as needed... # 4. If you need to remove and recreate + .\scripts\Remove-UEAddin.ps1 -AddinName "MyAddin" .\scripts\New-UEAddin.ps1 -AddinName "MyAddin" -Template "TemplateAddIn" -Module "Model" # 5. Final cleanup + .\scripts\Clear-UECustomizationEnvironment.ps1 ` @@ -308,8 +400,8 @@ These variables are available in the current session immediately and in future P ## Important Considerations ### Directory Structure -` -CustomizationRootDir/ + +`CustomizationRootDir/ source/ # Add-in source code directory TemplateAddIn/ # Template (preserved by Clear script) [AddinName]/ # Auto-generated copies with substitutions @@ -319,10 +411,10 @@ CustomizationRootDir/ DesignCustomization.xml, TagsCustomization.xml, ConfigurationCustomization.xml, ... [AddinName].uic [AddinName].dll - binaries/ # Supporting binaries from MSIX unpack -` + binaries/ # Supporting binaries from MSIX unpack` ### File Operations + - All scripts run non-interactively without prompting - Scripts validate paths and parameters before operations - AddinName validation enforces A-Za-z characters only @@ -330,6 +422,7 @@ CustomizationRootDir/ - Errors are thrown with clear messages about what went wrong ### Error Handling + - Scripts check for required files before operations - Missing files in remove operations generate warnings, not errors - Invalid paths or missing prerequisites generate clear error messages @@ -348,7 +441,7 @@ CustomizationRootDir/ 3. **Sequential Guidance**: Help users understand the order of operations. Setup must run before New-UEAddin. -4. **Patience with Long-Running Operations**: +4. **Patience with Long-Running Operations**: - The MSIX unpack operation (during Setup-UECustomizationEnvironment.ps1) can take several minutes to complete - Use appropriate timeouts (300000ms or 5 minutes minimum) when running setup scripts - **The scripts do NOT require any user input after they start** - all parameters are gathered upfront via `vscode/askQuestions` @@ -380,6 +473,7 @@ CustomizationRootDir/ ## When to Ask Clarifying Questions Ask for clarification when: + - User intent is ambiguous (is this first setup or modification?) - Required parameters are missing - Script prerequisites might not be met @@ -390,9 +484,9 @@ Example: "Have you already run Setup-UECustomizationEnvironment.ps1? The New-UEA ## Available Documentation The repository includes: + - **README.md**: Complete documentation of all scripts with detailed examples - **Instructions.md**: Original instructions and setup guidance - Each script is self-documented with inline comments Always leverage the README documentation when helping users understand requirements or troubleshoot issues. - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e5c015e..a2bdd2e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,50 +2,50 @@ name: Run Tests on: push: - branches: [ main, develop ] + branches: [main] pull_request: - branches: [ main, develop ] + branches: [main] jobs: test: runs-on: windows-latest steps: - - uses: actions/checkout@v4 - - - name: Set up PowerShell - shell: pwsh - run: | - Write-Host "PowerShell version: $($PSVersionTable.PSVersion)" - - - name: Install NuGet provider - shell: pwsh - run: | - if (-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) { - Write-Host "Installing NuGet provider..." - Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force | Out-Null - } - - - name: Trust PSGallery - shell: pwsh - run: | - $psGallery = Get-PSRepository -Name 'PSGallery' -ErrorAction SilentlyContinue - if ($psGallery -and $psGallery.InstallationPolicy -ne 'Trusted') { - Write-Host "Trusting PSGallery..." - Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted - } - - - name: Install Pester - shell: pwsh - run: | - if (-not (Get-Module -ListAvailable -Name Pester)) { - Write-Host "Installing Pester module..." - Install-Module Pester -Scope CurrentUser -Force -AllowClobber -SkipPublisherCheck | Out-Null - } - - - name: Run tests - shell: pwsh - run: | - cd ${{ github.workspace }} - & .\scripts\Tests\Run-Tests.ps1 -Quick - continue-on-error: false + - uses: actions/checkout@v4 + + - name: Set up PowerShell + shell: pwsh + run: | + Write-Host "PowerShell version: $($PSVersionTable.PSVersion)" + + - name: Install NuGet provider + shell: pwsh + run: | + if (-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) { + Write-Host "Installing NuGet provider..." + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force | Out-Null + } + + - name: Trust PSGallery + shell: pwsh + run: | + $psGallery = Get-PSRepository -Name 'PSGallery' -ErrorAction SilentlyContinue + if ($psGallery -and $psGallery.InstallationPolicy -ne 'Trusted') { + Write-Host "Trusting PSGallery..." + Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted + } + + - name: Install Pester + shell: pwsh + run: | + if (-not (Get-Module -ListAvailable -Name Pester)) { + Write-Host "Installing Pester module..." + Install-Module Pester -Scope CurrentUser -Force -AllowClobber -SkipPublisherCheck | Out-Null + } + + - name: Run tests + shell: pwsh + run: | + cd ${{ github.workspace }} + & .\scripts\Tests\Run-Tests.ps1 -Quick + continue-on-error: false diff --git a/.gitignore b/.gitignore index 18a9596..8b4abf6 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,9 @@ bld/ # Build results on 'Bin' directories **/[Bb]in/* +# Local UE customization runtime output +pmllib/ +shims/generated/ # Uncomment if you have tasks that rely on *.refresh files to move binaries # (https://github.com/github/gitignore/pull/3736) #!**/[Bb]in/*.refresh diff --git a/README.md b/README.md index 604efbd..e76eb9b 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ The UE Customization Manager agent: - **Explains workflows** - Helps you understand the proper sequence of operations - **Validates operations** - Warns about destructive operations and validates parameters - **Troubleshoots issues** - Provides context-aware assistance based on README documentation +- **Explains hosted validation** - Understands the fake-compile, shim, and hosted security-validation path used in CI - **Launches UE** - Can start AVEVA Unified Engineering when requested - **Manages the lifecycle** - From initial setup through add-in creation to final cleanup @@ -111,6 +112,12 @@ You can interact with the agent using natural language in the GitHub Copilot cha @ue-customization-manager The script says the environment doesn't exist @ue-customization-manager What files will be deleted if I run Clear? + +@ue-customization-manager Why did hosted security validation fail? + +@ue-customization-manager How does fake compile work for templates and Examples? + +@ue-customization-manager Why did a local pmllib folder appear after building a PML template? ``` ### Benefits of Using the Agent diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..ef2bc7b --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,194 @@ +# Hybrid validation pipeline for the GitHub-hosted UE customization repo. +# +# This pipeline is intended to be connected to GitHub through the Azure Pipelines GitHub App +# so its checks surface back into GitHub pull requests targeting main. +# +# The hosted test stage can run immediately on the Dabacon Products stateful hosted pools. +# The security stage remains opt-in, but now uses the hosted-compatible fake-compile flow +# on the Dabacon Products stateful hosted Ubuntu pool instead of generic vmImage selection. +# Templates is consumed from Dabacon Products/Templates. +# The Dabacon Products project still needs: +# - a self-hosted Windows pool with UE installed and MSBuild available +# - service connections named `Coverity on Polaris` and `BlackDuckScanner` + +resources: + repositories: + - repository: Templates + type: git + name: Dabacon Products/Templates + ref: refs/heads/feature/uecf-security-template-offload + - repository: antiMalwareTemplate + type: git + name: Architecture/Architecture + ref: refs/heads/antiMalwareVersions/latest + +variables: + - template: variables.yml@Templates + - name: CoverityProjectTeam + value: .RnD.Polaris.Dabacon-Products.SecurityAdvisors + - name: BlackDuckProjectName + value: AVEVA.UnifiedEngineeringCustomizationFramework + +trigger: + batch: true + branches: + include: + - main + +pr: + autoCancel: true + branches: + include: + - main + +stages: + - stage: Initialise + displayName: Initialise + jobs: + - template: jobs/publish-source.yml@Templates + parameters: + SourceFetchDepth: 1 + + - template: jobs/publish-templates.yml@Templates + + + - stage: BuildWithShims + displayName: Build With Shims + dependsOn: Initialise + jobs: + - job: BuildWithShims + displayName: Build With Shims + timeoutInMinutes: 120 + pool: + name: Dabacon-Products-Microsoft-Hosted-Ubuntu + demands: + - ImageOverride -equals ubuntu-latest + - WorkFolder -equals /mnt/storage/sdc/storage + container: mcr.microsoft.com/dotnet/sdk:9.0 + workspace: + clean: all + steps: + - template: steps/retrieve-source.yml@Templates + parameters: + IsIPR: false + ArtifactName: Source + DownloadPath: $(Pipeline.Workspace)/Source + + - download: current + artifact: Templates + displayName: Download Templates artifact + patterns: "**" + + - task: UseDotNet@2 + displayName: Ensure .NET 9 SDK for shim build + retryCountOnTaskFailure: 3 + inputs: + packageType: sdk + version: 9.0.x + + - template: shims/shim-projects.yml + parameters: + OutputPath: $(Pipeline.Workspace)/Source/.tmp/generated-shim-manifest.json + + - template: shims/compile-with-shims.yml + parameters: + Directory: templates,Examples + Configuration: Release + ShimManifestPath: $(Pipeline.Workspace)/Source/.tmp/generated-shim-manifest.json + GeneratedRunnerPath: $(Pipeline.Workspace)/Source/.tmp/generated-fake-ue-compile.ps1 + GeneratedShimProjectsPath: $(Pipeline.Workspace)/Source/.tmp/generated-shims + GeneratedShimSourcesPath: $(Pipeline.Workspace)/Source/.tmp/generated-shim-sources + GeneratedShimPropsPath: $(Pipeline.Workspace)/Source/.tmp/generated-shim-props/Directory.Build.props + ShimOutputPath: $(Pipeline.Workspace)/Source/shims/artifacts/Release + + - publish: $(Pipeline.Workspace)/Source/shims/artifacts/Release + artifact: Build.Release + displayName: Publish build artifact for component template comparison + + - stage: HostedTests + displayName: Hosted Tests + dependsOn: Initialise + jobs: + - job: Pester + displayName: Pester + pool: + name: Dabacon-Products-Microsoft-Hosted-Ubuntu + demands: + - ImageOverride -equals ubuntu-latest + - WorkFolder -equals /mnt/storage/sdc/storage + container: mcr.microsoft.com/dotnet/sdk:9.0 + workspace: + clean: all + steps: + - template: steps/retrieve-source.yml@Templates + parameters: + IsIPR: false + ArtifactName: Source + DownloadPath: $(Pipeline.Workspace)/Source + + - pwsh: | + Write-Host "PowerShell version: $($PSVersionTable.PSVersion)" + displayName: Show PowerShell version + + - pwsh: | + if (-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) { + Write-Host "Installing NuGet provider..." + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force | Out-Null + } + displayName: Install NuGet provider + + - pwsh: | + $psGallery = Get-PSRepository -Name 'PSGallery' -ErrorAction SilentlyContinue + if ($psGallery -and $psGallery.InstallationPolicy -ne 'Trusted') { + Write-Host "Trusting PSGallery..." + Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted + } + displayName: Trust PSGallery + + - pwsh: | + if (-not (Get-Module -ListAvailable -Name Pester)) { + Write-Host "Installing Pester module..." + Install-Module Pester -Scope CurrentUser -Force -AllowClobber -SkipPublisherCheck | Out-Null + } + displayName: Install Pester + + - pwsh: | + ./scripts/Tests/Run-Tests.ps1 -Quick + displayName: Run tests + workingDirectory: $(Pipeline.Workspace)/Source + + - template: uecf/component-coverity-stage.yml@Templates + parameters: + CoverityTeamName: $(CoverityProjectTeam) + ProjectName: $(BlackDuckProjectName) + + - stage: AntiMalware + displayName: AntiMalware + dependsOn: + - Initialise + - BuildWithShims + jobs: + - template: jobs/antimalware.yml@Templates + parameters: + JobName: AntiMalwareScan + + - stage: CodeAnalysisLogs + displayName: CodeAnalysisLogs + dependsOn: AntiMalware + jobs: + - template: jobs/publish-CodeAnalysisLogs.yml@Templates + parameters: + Analysers: + [] + + - stage: BlackDuck + displayName: BlackDuck + dependsOn: BuildWithShims + jobs: + - template: component-blackduck.yml@Templates + parameters: + BuildArtifact: Build.Release + BlackDuckGroup: DabaconGroup + IsIPR: false + ProjectName: $(BlackDuckProjectName) + PackageVersion: $(Build.SourceBranchName)-$(Build.BuildId) diff --git a/scripts/Build-AddIns.ps1 b/scripts/Build-AddIns.ps1 index 932ddde..a329f78 100644 --- a/scripts/Build-AddIns.ps1 +++ b/scripts/Build-AddIns.ps1 @@ -1,7 +1,9 @@ param( [Parameter(Mandatory = $false)] [string]$Directory = "source", - [string]$Configuration = "Debug" + [string]$Configuration = "Debug", + [switch]$SkipPostBuildEvent, + [switch]$Rebuild ) $ErrorActionPreference = "Stop" @@ -33,7 +35,33 @@ if (-not $projects) { $failed = @() foreach ($proj in $projects) { Write-Host "--- Building $($proj.FullName) ---" -ForegroundColor Cyan - dotnet build $proj.FullName -c $Configuration -clp:Summary -nologo + $buildArgs = @( + 'build' + $proj.FullName + '-c' + $Configuration + '-clp:Summary' + '-nologo' + ) + + if ($env:FrameworkPathOverride) { + $buildArgs += "-p:FrameworkPathOverride=$($env:FrameworkPathOverride)" + } + + if ($env:TargetFrameworkRootPath) { + $buildArgs += "-p:TargetFrameworkRootPath=$($env:TargetFrameworkRootPath)" + } + + if ($Rebuild) { + $buildArgs += '-t:Rebuild' + } + + if ($SkipPostBuildEvent) { + $buildArgs += '-p:RunPostBuildEvent=Never' + $buildArgs += '-p:PostBuildEvent=' + } + + dotnet @buildArgs if ($LASTEXITCODE -ne 0) { $failed += $proj.FullName } diff --git a/scripts/Tests/UECustomizationFunctions.Tests.ps1 b/scripts/Tests/UECustomizationFunctions.Tests.ps1 index 64fcef1..bb8a9d8 100644 --- a/scripts/Tests/UECustomizationFunctions.Tests.ps1 +++ b/scripts/Tests/UECustomizationFunctions.Tests.ps1 @@ -5,6 +5,8 @@ Import-Module -Name $modulePath -Force -ErrorAction Stop Describe 'UECustomizationFunctions Module' { BeforeAll { + $script:TempRoot = [System.IO.Path]::GetTempPath() + Mock -CommandName Set-EnvironmentVariableUserAndSession -ModuleName UECustomizationFunctions -MockWith { Set-Item -Path ("Env:{0}" -f $Name) -Value $Value } @@ -45,7 +47,7 @@ Describe 'UECustomizationFunctions Module' { Context 'Find-RequiredFile' { It 'throws if file not found' { - $temp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("UE-Test-" + [Guid]::NewGuid())) + $temp = New-Item -ItemType Directory -Path (Join-Path $script:TempRoot ("UE-Test-" + [Guid]::NewGuid())) try { { Find-RequiredFile -RootPath $temp.FullName -FileName 'nope.txt' } | Should -Throw } @@ -55,7 +57,7 @@ Describe 'UECustomizationFunctions Module' { } It 'returns full path when file exists' { - $temp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("UE-Test-" + [Guid]::NewGuid())) + $temp = New-Item -ItemType Directory -Path (Join-Path $script:TempRoot ("UE-Test-" + [Guid]::NewGuid())) try { $file = Join-Path $temp.FullName 'found.txt' Set-Content -Path $file -Value 'test' -Encoding UTF8 @@ -68,7 +70,7 @@ Describe 'UECustomizationFunctions Module' { } It 'finds files in subdirectories' { - $temp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("UE-Test-" + [Guid]::NewGuid())) + $temp = New-Item -ItemType Directory -Path (Join-Path $script:TempRoot ("UE-Test-" + [Guid]::NewGuid())) try { $subdir = New-Item -Path (Join-Path $temp.FullName "subdir") -ItemType Directory $file = Join-Path $subdir.FullName 'nested.txt' @@ -84,7 +86,7 @@ Describe 'UECustomizationFunctions Module' { Context 'Initialize-DirectoryStructure' { It 'creates addins and pmllib directories' { - $tempRoot = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("UE-Test-" + [Guid]::NewGuid())) + $tempRoot = New-Item -ItemType Directory -Path (Join-Path $script:TempRoot ("UE-Test-" + [Guid]::NewGuid())) try { $addins = Join-Path $tempRoot.FullName 'addins' $pmllib = Join-Path $tempRoot.FullName 'pmllib' @@ -98,7 +100,7 @@ Describe 'UECustomizationFunctions Module' { } It 'creates nested directory paths' { - $tempRoot = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("UE-Test-" + [Guid]::NewGuid())) + $tempRoot = New-Item -ItemType Directory -Path (Join-Path $script:TempRoot ("UE-Test-" + [Guid]::NewGuid())) try { $addins = Join-Path $tempRoot.FullName 'nested\path\addins' $pmllib = Join-Path $tempRoot.FullName 'nested\path\pmllib' @@ -114,7 +116,7 @@ Describe 'UECustomizationFunctions Module' { Context 'Set-EnvironmentConfiguration' { It 'creates configuration file with correct content' { - $tempRoot = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("UE-Test-" + [Guid]::NewGuid())) + $tempRoot = New-Item -ItemType Directory -Path (Join-Path $script:TempRoot ("UE-Test-" + [Guid]::NewGuid())) try { $configPath = Join-Path $tempRoot.FullName 'environment.json' $addins = Join-Path $tempRoot.FullName 'addins' @@ -134,7 +136,7 @@ Describe 'UECustomizationFunctions Module' { } It 'sets environment variables in session' { - $tempRoot = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("UE-Test-" + [Guid]::NewGuid())) + $tempRoot = New-Item -ItemType Directory -Path (Join-Path $script:TempRoot ("UE-Test-" + [Guid]::NewGuid())) try { $configPath = Join-Path $tempRoot.FullName 'environment.json' $addins = Join-Path $tempRoot.FullName 'addins' @@ -159,7 +161,7 @@ Describe 'UECustomizationFunctions Module' { } It 'restores environment variables from config file' { - $tempRoot = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("UE-Test-" + [Guid]::NewGuid())) + $tempRoot = New-Item -ItemType Directory -Path (Join-Path $script:TempRoot ("UE-Test-" + [Guid]::NewGuid())) try { $configPath = Join-Path $tempRoot.FullName 'environment.json' $addins = Join-Path $tempRoot.FullName 'addins' @@ -193,7 +195,7 @@ Describe 'UECustomizationFunctions Module' { } It 'throws if destination is invalid' { - $temp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("UE-Test-" + [Guid]::NewGuid())) + $temp = New-Item -ItemType Directory -Path (Join-Path $script:TempRoot ("UE-Test-" + [Guid]::NewGuid())) try { $zipPath = Join-Path $temp.FullName 'test.zip' Set-Content -Path $zipPath -Value 'dummy' @@ -209,10 +211,10 @@ Describe 'UECustomizationFunctions Module' { Context 'Install-UEFiles' { It 'does not throw if source directory has no config files' { - $temp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("UE-Test-" + [Guid]::NewGuid())) + $temp = New-Item -ItemType Directory -Path (Join-Path $script:TempRoot ("UE-Test-" + [Guid]::NewGuid())) try { # Should warn but not throw when no config files are found - { Install-UEFiles -UnpackPath $temp.FullName -AddinsFolder (Join-Path $env:TEMP 'addins') } | Should -Not -Throw + { Install-UEFiles -UnpackPath $temp.FullName -AddinsFolder (Join-Path $script:TempRoot 'addins') } | Should -Not -Throw } finally { Remove-Item -Path $temp.FullName -Recurse -Force @@ -228,7 +230,7 @@ Describe 'UECustomizationFunctions Module' { Context 'Clear-UECustomizationEnvironment.ps1 Script' { It 'removes environment.json during cleanup' { - $tempRoot = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("UE-Test-" + [Guid]::NewGuid())) + $tempRoot = New-Item -ItemType Directory -Path (Join-Path $script:TempRoot ("UE-Test-" + [Guid]::NewGuid())) try { # Create a mock environment.json $configPath = Join-Path $tempRoot.FullName 'environment.json' @@ -267,7 +269,7 @@ Describe 'UECustomizationFunctions Module' { } It 'updates DesignCustomization.xml only when template contains .uic' { - $tempRoot = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("UE-Test-" + [Guid]::NewGuid())) + $tempRoot = New-Item -ItemType Directory -Path (Join-Path $script:TempRoot ("UE-Test-" + [Guid]::NewGuid())) try { # Arrange environment structure $addins = Join-Path $tempRoot.FullName 'addins' diff --git a/scripts/UECustomizationFunctions.psm1 b/scripts/UECustomizationFunctions.psm1 index 3c5c361..5c8ae82 100644 --- a/scripts/UECustomizationFunctions.psm1 +++ b/scripts/UECustomizationFunctions.psm1 @@ -808,20 +808,38 @@ function Invoke-EnvironmentCleanup { $excludeDirs = @('bin', 'obj', '.vs', 'logs', 'log') $excludeFiles = @('*.user', '*.suo', '*.rsuser', '*.userprefs', '*.pdb', '*.ipdb', '*.iobj', '*.obj', '*.pch', '*.log', '*.tlog', '*.tmp', '*.cache', '*.ilk') - $xdArgs = $excludeDirs | ForEach-Object { '/XD', $_ } - $xfArgs = $excludeFiles | ForEach-Object { '/XF', $_ } - $robocopyArgs = @($sourceTemplateDir, $targetAddinDir, '/E', '/NFL', '/NDL', '/NJH', '/NJS', '/NC', '/NS', '/R:2', '/W:1') + $xdArgs + $xfArgs - & robocopy @robocopyArgs | Out-Null - $rc = $LASTEXITCODE - if ($rc -ge 8) { - $exception = [System.InvalidOperationException]::new("Robocopy failed with exit code $rc while copying template.") - $errorRecord = [System.Management.Automation.ErrorRecord]::new( - $exception, - 'RobocopyFailed', - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $rc - ) - $PSCmdlet.ThrowTerminatingError($errorRecord) + $robocopyCommand = Get-Command -Name 'robocopy' -ErrorAction SilentlyContinue + if ($robocopyCommand) { + $xdArgs = $excludeDirs | ForEach-Object { '/XD', $_ } + $xfArgs = $excludeFiles | ForEach-Object { '/XF', $_ } + $robocopyArgs = @($sourceTemplateDir, $targetAddinDir, '/E', '/NFL', '/NDL', '/NJH', '/NJS', '/NC', '/NS', '/R:2', '/W:1') + $xdArgs + $xfArgs + & $robocopyCommand.Source @robocopyArgs | Out-Null + $rc = $LASTEXITCODE + if ($rc -ge 8) { + $exception = [System.InvalidOperationException]::new("Robocopy failed with exit code $rc while copying template.") + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + $exception, + 'RobocopyFailed', + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $rc + ) + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + } + else { + Copy-Item -Path $sourceTemplateDir -Destination $targetAddinDir -Recurse -Force + + foreach ($excludedDir in $excludeDirs) { + Get-ChildItem -Path $targetAddinDir -Directory -Recurse -Force -ErrorAction SilentlyContinue | + Where-Object { $_.Name -eq $excludedDir } | + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + } + + foreach ($excludedPattern in $excludeFiles) { + Get-ChildItem -Path $targetAddinDir -File -Recurse -Force -ErrorAction SilentlyContinue | + Where-Object { $_.Name -like $excludedPattern } | + Remove-Item -Force -ErrorAction SilentlyContinue + } } $itemsToRename = Get-ChildItem -Path $targetAddinDir -Recurse -Force | Where-Object { $_.Name -like "*$Template*" } diff --git a/shims/compile-with-shims.yml b/shims/compile-with-shims.yml new file mode 100644 index 0000000..d3ff76a --- /dev/null +++ b/shims/compile-with-shims.yml @@ -0,0 +1,452 @@ +parameters: + - name: Directory + type: string + default: templates,Examples + - name: Configuration + type: string + default: Release + - name: ShimManifestPath + type: string + default: $(Pipeline.Workspace)/Source/.tmp/generated-shim-manifest.json + - name: GeneratedRunnerPath + type: string + default: $(Pipeline.Workspace)/Source/.tmp/generated-fake-ue-compile.ps1 + - name: GeneratedShimProjectsPath + type: string + default: $(Pipeline.Workspace)/Source/.tmp/generated-shims + - name: GeneratedShimSourcesPath + type: string + default: $(Pipeline.Workspace)/Source/.tmp/generated-shim-sources + - name: GeneratedShimPropsPath + type: string + default: $(Pipeline.Workspace)/Source/.tmp/generated-shim-props/Directory.Build.props + - name: ShimOutputPath + type: string + default: $(Pipeline.Workspace)/Source/shims/artifacts/Release + - name: Rebuild + type: boolean + default: false + +steps: + - pwsh: | + $generatedRunnerPath = '${{ parameters.GeneratedRunnerPath }}' + $generatedRunnerDirectory = Split-Path -Path $generatedRunnerPath -Parent + if (-not [string]::IsNullOrWhiteSpace($generatedRunnerDirectory)) { + $null = New-Item -ItemType Directory -Path $generatedRunnerDirectory -Force + } + + $runnerScript = @' + #Requires -Version 7.0 + + [CmdletBinding()] + param( + [string[]]$Directory = @('Examples'), + [string]$Configuration = 'Release', + [string]$ShimProjectsPath, + [string]$ShimManifestPath, + [string]$GeneratedShimProjectsPath, + [string]$GeneratedShimSourcesPath, + [string]$GeneratedShimPropsPath, + [string]$ShimOutputPath, + [switch]$Rebuild + ) + + $ErrorActionPreference = 'Stop' + + $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path + $defaultGeneratedShimManifestPath = Join-Path $repoRoot '.tmp\generated-shim-manifest.json' + $defaultGeneratedShimProjectsPath = Join-Path $repoRoot '.tmp\generated-shims' + $defaultGeneratedShimSourcesPath = Join-Path $repoRoot '.tmp\generated-shim-sources' + $defaultGeneratedShimPropsPath = Join-Path $repoRoot '.tmp\generated-shim-props\Directory.Build.props' + + if (-not $ShimProjectsPath) { + $ShimProjectsPath = Join-Path $repoRoot 'shims' + } + + if (-not $ShimManifestPath) { + if (Test-Path -LiteralPath $defaultGeneratedShimManifestPath) { + $ShimManifestPath = $defaultGeneratedShimManifestPath + } + else { + throw ( + 'Shim manifest path was not provided and no generated manifest was found. ' + + 'Generate .tmp/generated-shim-manifest.json from shims/shim-projects.yml ' + + 'or pass -ShimManifestPath explicitly.' + ) + } + } + + if (-not $GeneratedShimProjectsPath) { + $GeneratedShimProjectsPath = $defaultGeneratedShimProjectsPath + } + + if (-not $GeneratedShimSourcesPath) { + $GeneratedShimSourcesPath = $defaultGeneratedShimSourcesPath + } + + if (-not $GeneratedShimPropsPath) { + $GeneratedShimPropsPath = $defaultGeneratedShimPropsPath + } + + if (-not $ShimOutputPath) { + $ShimOutputPath = Join-Path $ShimProjectsPath "artifacts\$Configuration" + } + + $resolvedShimProjectsPath = (Resolve-Path $ShimProjectsPath).Path + $resolvedShimManifestPath = (Resolve-Path $ShimManifestPath).Path + $resolvedGeneratedShimProjectsPath = [System.IO.Path]::GetFullPath($GeneratedShimProjectsPath) + $resolvedGeneratedShimSourcesPath = [System.IO.Path]::GetFullPath($GeneratedShimSourcesPath) + $resolvedGeneratedShimPropsPath = [System.IO.Path]::GetFullPath($GeneratedShimPropsPath) + $resolvedShimOutputDirectory = [System.IO.Path]::GetFullPath($ShimOutputPath) + $resolvedShimPropsDirectory = Split-Path -Path $resolvedGeneratedShimPropsPath -Parent + $buildScriptPath = Join-Path $repoRoot 'scripts\Build-AddIns.ps1' + + function Ensure-NetFrameworkReferenceAssemblies { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$WorkspaceRoot + ) + + $packageId = 'Microsoft.NETFramework.ReferenceAssemblies.net481' + $packageVersion = '1.0.3' + $packageRoot = Join-Path $WorkspaceRoot '.tmp\netfx-reference-assemblies' + $packageDir = Join-Path $packageRoot "$packageId.$packageVersion" + $packageArchive = Join-Path $packageRoot "$packageId.$packageVersion.nupkg" + $referenceDir = Join-Path $packageDir 'build\.NETFramework\v4.8.1' + $frameworkRoot = Join-Path $packageDir 'build\' + + if (-not (Test-Path -LiteralPath $referenceDir)) { + $null = New-Item -ItemType Directory -Path $packageRoot -Force + + $packageUrl = "https://www.nuget.org/api/v2/package/$packageId/$packageVersion" + Write-Host ("Downloading .NET Framework reference assemblies from {0}" -f $packageUrl) -ForegroundColor Cyan + Invoke-WebRequest -Uri $packageUrl -OutFile $packageArchive + + if (Test-Path -LiteralPath $packageDir) { + Remove-Item -LiteralPath $packageDir -Recurse -Force + } + + Expand-Archive -Path $packageArchive -DestinationPath $packageDir -Force + } + + if (-not (Test-Path -LiteralPath $referenceDir)) { + throw "Reference assembly directory not found after package extraction: $referenceDir" + } + + return [PSCustomObject]@{ + FrameworkPathOverride = $referenceDir + TargetFrameworkRootPath = $frameworkRoot + } + } + + function Import-ShimManifest { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + $extension = [System.IO.Path]::GetExtension($Path) + switch ($extension.ToLowerInvariant()) { + '.json' { + return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -AsHashtable + } + '.psd1' { + return Import-PowerShellDataFile -Path $Path + } + default { + throw "Unsupported shim manifest format '$extension'. Supported formats are .json and .psd1." + } + } + } + + $shimManifest = Import-ShimManifest -Path $resolvedShimManifestPath + $shimProjects = if ($shimManifest.ContainsKey('Projects')) { + $shimManifest.Projects + } + else { + $null + } + + if (-not $shimProjects -or $shimProjects.Count -eq 0) { + throw "No shim projects were defined in $resolvedShimManifestPath" + } + + if (Test-Path -LiteralPath $resolvedGeneratedShimProjectsPath) { + Remove-Item -LiteralPath $resolvedGeneratedShimProjectsPath -Recurse -Force + } + + if (Test-Path -LiteralPath $resolvedGeneratedShimSourcesPath) { + Remove-Item -LiteralPath $resolvedGeneratedShimSourcesPath -Recurse -Force + } + + if (Test-Path -LiteralPath $resolvedShimPropsDirectory) { + Remove-Item -LiteralPath $resolvedShimPropsDirectory -Recurse -Force + } + + $null = New-Item -ItemType Directory -Path $resolvedGeneratedShimProjectsPath -Force + $null = New-Item -ItemType Directory -Path $resolvedGeneratedShimSourcesPath -Force + $null = New-Item -ItemType Directory -Path $resolvedShimPropsDirectory -Force + + @( + '', + ' ', + ' net48', + ' latest', + ' disable', + ' disable', + ' true', + ' false', + ' false', + ' 1591', + ' $(MSBuildThisFileDirectory)..\..\shims\artifacts\$(Configuration)\', + ' false', + ' false', + ' ', + '' + ) -join [Environment]::NewLine | Set-Content -LiteralPath $resolvedGeneratedShimPropsPath -Encoding utf8 + + foreach ($projectEntry in $shimProjects.GetEnumerator()) { + $projectName = [string]$projectEntry.Key + $projectConfig = $projectEntry.Value + $rootNamespace = if ($projectConfig.RootNamespace) { + [string]$projectConfig.RootNamespace + } + else { + $projectName + } + + $projectLines = [System.Collections.Generic.List[string]]::new() + $projectLines.Add('') + $projectLines.Add((' ' -f $resolvedGeneratedShimPropsPath.Replace('&', '&'))) + $projectLines.Add(' ') + $projectLines.Add(" $projectName") + $projectLines.Add(" $rootNamespace") + $projectLines.Add(' ') + + $sourcePaths = [System.Collections.Generic.List[string]]::new() + if ($projectConfig.ContainsKey('SourceFiles')) { + $projectSourceRoot = Join-Path $resolvedGeneratedShimSourcesPath $projectName + $null = New-Item -ItemType Directory -Path $projectSourceRoot -Force + + foreach ($sourceFile in @($projectConfig.SourceFiles)) { + if ($null -eq $sourceFile) { + continue + } + + $sourceRelativePath = [string]$sourceFile.Path + if ([string]::IsNullOrWhiteSpace($sourceRelativePath)) { + continue + } + + $sourceOutputPath = Join-Path $projectSourceRoot $sourceRelativePath + $sourceOutputDirectory = Split-Path -Path $sourceOutputPath -Parent + if (-not [string]::IsNullOrWhiteSpace($sourceOutputDirectory)) { + $null = New-Item -ItemType Directory -Path $sourceOutputDirectory -Force + } + + $sourceContent = if ($sourceFile.ContainsKey('Content')) { + [string]$sourceFile.Content + } + else { + [string]$sourceFile.content + } + + Set-Content -LiteralPath $sourceOutputPath -Value $sourceContent -Encoding utf8 + $null = $sourcePaths.Add($sourceOutputPath) + } + } + + if ($sourcePaths.Count -gt 0) { + $projectLines.Add(' ') + foreach ($sourcePath in $sourcePaths) { + $projectLines.Add((' ' -f $sourcePath.Replace('&', '&'))) + } + $projectLines.Add(' ') + } + + if ($projectConfig.ContainsKey('ProjectReferences') -and @($projectConfig.ProjectReferences).Count -gt 0) { + $projectLines.Add(' ') + foreach ($projectReference in @($projectConfig.ProjectReferences)) { + $projectReferenceName = [string]$projectReference + $projectReferenceDllPath = Join-Path $resolvedShimOutputDirectory "$projectReferenceName.dll" + $projectLines.Add((' ' -f $projectReferenceName.Replace('&', '&'))) + $projectLines.Add((' {0}' -f $projectReferenceDllPath.Replace('&', '&'))) + $projectLines.Add(' false') + $projectLines.Add(' ') + } + $projectLines.Add(' ') + } + + $projectLines.Add('') + + $generatedProjectPath = Join-Path $resolvedGeneratedShimProjectsPath "$projectName.csproj" + $projectLines -join [Environment]::NewLine | Set-Content -LiteralPath $generatedProjectPath -Encoding utf8 + } + + function Add-ShimProjectBuildOrder { + param( + [Parameter(Mandatory = $true)] + [string]$ProjectName, + + [Parameter(Mandatory = $true)] + [hashtable]$ProjectsByName, + + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [System.Collections.Generic.HashSet[string]]$Visiting, + + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [System.Collections.Generic.HashSet[string]]$Visited, + + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [System.Collections.Generic.List[string]]$BuildOrder + ) + + if ($Visited.Contains($ProjectName)) { + return + } + + if (-not $Visiting.Add($ProjectName)) { + throw "Detected circular shim dependency involving '$ProjectName'" + } + + $projectConfig = $ProjectsByName[$ProjectName] + if ($projectConfig -and $projectConfig.ContainsKey('ProjectReferences')) { + foreach ($projectReference in @($projectConfig.ProjectReferences)) { + $projectReferenceName = [string]$projectReference + if (-not $ProjectsByName.ContainsKey($projectReferenceName)) { + throw "Shim project '$ProjectName' references undefined shim project '$projectReferenceName'" + } + + Add-ShimProjectBuildOrder -ProjectName $projectReferenceName -ProjectsByName $ProjectsByName -Visiting $Visiting -Visited $Visited -BuildOrder $BuildOrder + } + } + + $null = $Visiting.Remove($ProjectName) + $null = $Visited.Add($ProjectName) + $BuildOrder.Add($ProjectName) + } + + $orderedProjectNames = [System.Collections.Generic.List[string]]::new() + $visitingProjects = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + $visitedProjects = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($projectName in $shimProjects.Keys) { + Add-ShimProjectBuildOrder -ProjectName ([string]$projectName) -ProjectsByName $shimProjects -Visiting $visitingProjects -Visited $visitedProjects -BuildOrder $orderedProjectNames + } + + $projects = foreach ($projectName in $orderedProjectNames) { + Get-Item -LiteralPath (Join-Path $resolvedGeneratedShimProjectsPath "$projectName.csproj") + } + if (-not $projects) { + throw "No generated shim projects found under $resolvedGeneratedShimProjectsPath" + } + + foreach ($project in $projects) { + Write-Host ("Building shim assembly: {0}" -f $project.Name) -ForegroundColor Cyan + $shimBuildArgs = 'build', $project.FullName, '-c', $Configuration, '-nologo' + + if ($Rebuild) { + $shimBuildArgs += '-t:Rebuild' + } + + dotnet @shimBuildArgs + if ($LASTEXITCODE -ne 0) { + throw ("Shim build failed: {0}" -f $project.FullName) + } + } + + $resolvedShimOutputPath = (Resolve-Path $ShimOutputPath).Path + $previousReferencePath = $env:UNIFIED_ENGINEERING_REFERENCE_PATH + $previousFrameworkPathOverride = $env:FrameworkPathOverride + $previousTargetFrameworkRootPath = $env:TargetFrameworkRootPath + $targetDirectories = foreach ($entry in $Directory) { + foreach ($candidate in ($entry -split ',')) { + $trimmed = $candidate.Trim().Trim([char[]]@("'", '"')) + if (-not [string]::IsNullOrWhiteSpace($trimmed)) { + $trimmed + } + } + } + + try { + $env:UNIFIED_ENGINEERING_REFERENCE_PATH = $resolvedShimOutputPath + + if (-not $IsWindows) { + $netFrameworkReferences = Ensure-NetFrameworkReferenceAssemblies -WorkspaceRoot $repoRoot + $env:FrameworkPathOverride = $netFrameworkReferences.FrameworkPathOverride + $env:TargetFrameworkRootPath = $netFrameworkReferences.TargetFrameworkRootPath + + Write-Host ("Using .NET Framework reference assemblies from: {0}" -f $env:FrameworkPathOverride) -ForegroundColor Yellow + } + + if ($env:TF_BUILD) { + Write-Host ("##vso[task.setvariable variable=UNIFIED_ENGINEERING_REFERENCE_PATH]{0}" -f $resolvedShimOutputPath) + } + + Write-Host ("Using fake UE references from: {0}" -f $resolvedShimOutputPath) -ForegroundColor Yellow + + foreach ($targetDirectory in $targetDirectories) { + Write-Host ("Compiling with fake UE references: {0}" -f $targetDirectory) -ForegroundColor Yellow + & $buildScriptPath -Directory $targetDirectory -Configuration $Configuration -SkipPostBuildEvent -Rebuild:$Rebuild + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + } + + exit 0 + } + finally { + if ([string]::IsNullOrWhiteSpace($previousReferencePath)) { + Remove-Item Env:UNIFIED_ENGINEERING_REFERENCE_PATH -ErrorAction SilentlyContinue + } + else { + $env:UNIFIED_ENGINEERING_REFERENCE_PATH = $previousReferencePath + } + + if ([string]::IsNullOrWhiteSpace($previousFrameworkPathOverride)) { + Remove-Item Env:FrameworkPathOverride -ErrorAction SilentlyContinue + } + else { + $env:FrameworkPathOverride = $previousFrameworkPathOverride + } + + if ([string]::IsNullOrWhiteSpace($previousTargetFrameworkRootPath)) { + Remove-Item Env:TargetFrameworkRootPath -ErrorAction SilentlyContinue + } + else { + $env:TargetFrameworkRootPath = $previousTargetFrameworkRootPath + } + } + '@ + + Set-Content -LiteralPath $generatedRunnerPath -Value $runnerScript -Encoding utf8 + displayName: Generate fake UE compile helper + + - ${{ if ne(parameters.Rebuild, true) }}: + - pwsh: | + & '${{ parameters.GeneratedRunnerPath }}' ` + -Directory '${{ parameters.Directory }}' ` + -Configuration '${{ parameters.Configuration }}' ` + -ShimManifestPath '${{ parameters.ShimManifestPath }}' ` + -GeneratedShimProjectsPath '${{ parameters.GeneratedShimProjectsPath }}' ` + -GeneratedShimSourcesPath '${{ parameters.GeneratedShimSourcesPath }}' ` + -GeneratedShimPropsPath '${{ parameters.GeneratedShimPropsPath }}' ` + -ShimOutputPath '${{ parameters.ShimOutputPath }}' + displayName: Run fake UE compile helper + + - ${{ if eq(parameters.Rebuild, true) }}: + - pwsh: | + & '${{ parameters.GeneratedRunnerPath }}' ` + -Directory '${{ parameters.Directory }}' ` + -Configuration '${{ parameters.Configuration }}' ` + -ShimManifestPath '${{ parameters.ShimManifestPath }}' ` + -GeneratedShimProjectsPath '${{ parameters.GeneratedShimProjectsPath }}' ` + -GeneratedShimSourcesPath '${{ parameters.GeneratedShimSourcesPath }}' ` + -GeneratedShimPropsPath '${{ parameters.GeneratedShimPropsPath }}' ` + -ShimOutputPath '${{ parameters.ShimOutputPath }}' ` + -Rebuild + displayName: Run fake UE compile helper (Rebuild) diff --git a/shims/shim-projects.yml b/shims/shim-projects.yml new file mode 100644 index 0000000..d246f19 --- /dev/null +++ b/shims/shim-projects.yml @@ -0,0 +1,1802 @@ +parameters: + - name: OutputPath + type: string + default: $(Pipeline.Workspace)/Source/.tmp/generated-shim-manifest.json + - name: Shims + type: object + default: + - name: "Aveva.ApplicationFramework" + rootNamespace: "" + sourceFiles: + - path: "src/ApplicationFramework.cs" + content: |- + using System; + using System.Collections.Generic; + + namespace Aveva.ApplicationFramework + { + public interface IAddinInjected + { + string Name { get; } + + string Description { get; } + + void Start(IDependencyResolver resolver); + + void Start(ServiceManager serviceManager); + + void Stop(); + } + + public interface IDependencyResolver + { + } + + public sealed class ServiceManager + { + } + + public static class DependencyResolver + { + private static readonly Aveva.ApplicationFramework.Presentation.WindowManager s_windowManager = new Aveva.ApplicationFramework.Presentation.WindowManager(); + private static readonly Aveva.ApplicationFramework.Presentation.CommandManager s_commandManager = new Aveva.ApplicationFramework.Presentation.CommandManager(); + private static readonly Dictionary s_services = new Dictionary + { + { typeof(Aveva.ApplicationFramework.Presentation.IWindowManager), s_windowManager }, + { typeof(Aveva.ApplicationFramework.Presentation.ICommandManager), s_commandManager }, + { typeof(Aveva.ApplicationFramework.Presentation.ICommandBarManager), s_windowManager.CommandBarManager }, + }; + + public static T GetImplementationOf() where T : class + { + if (s_services.TryGetValue(typeof(T), out object service)) + { + return service as T; + } + + return null; + } + } + + public static class Log + { + private static readonly LoggerProxy s_logger = new LoggerProxy(); + + public static LoggerProxy Logger() + { + return s_logger; + } + } + + public sealed class LoggerProxy + { + public void MessageFormat(string format, params object[] args) + { + } + } + } + projectReferences: + - "Aveva.ApplicationFramework.Presentation" + - name: "Aveva.ApplicationFramework.Implementation" + rootNamespace: "" + sourceFiles: [] + projectReferences: [] + - name: "Aveva.ApplicationFramework.Presentation" + rootNamespace: "" + sourceFiles: + - path: "src/ApplicationFrameworkPresentation.cs" + content: |- + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Windows.Forms; + + namespace Aveva.ApplicationFramework.Presentation + { + public class Command + { + public string Key { get; set; } + + public IList List { get; } = new List(); + + public object Value { get; set; } + + public bool Checked { get; set; } + + public virtual void Execute() + { + } + } + + public enum DockedPosition + { + Left, + Right, + Top, + Bottom, + } + + public class DockedWindow + { + public event CancelEventHandler Closing; + + public event EventHandler Closed; + + public string Key { get; set; } + + public string Title { get; set; } + + public Control Content { get; set; } + + public DockedPosition Position { get; set; } + + public int Width { get; set; } + + public bool SaveLayout { get; set; } + + public bool Visible { get; private set; } + + public void Show() + { + Visible = true; + } + + public void Hide() + { + Visible = false; + } + + public void Close() + { + CancelEventArgs args = new CancelEventArgs(); + Closing?.Invoke(this, args); + if (args.Cancel) + { + return; + } + + Visible = false; + Closed?.Invoke(this, EventArgs.Empty); + } + } + + public interface IWindowManager + { + event EventHandler WindowLayoutLoaded; + + ICommandBarManager CommandBarManager { get; } + + DockedWindow CreateDockedWindow(string key, string title, Control control, DockedPosition position); + } + + public interface ICommandManager + { + CommandCollection Commands { get; } + } + + public interface ICommandBarManager + { + event EventHandler UILoaded; + + ToolCollection RootTools { get; } + } + + public interface ITool + { + string Key { get; set; } + + bool Checked { get; set; } + + event EventHandler ToolClick; + } + + public sealed class CommandCollection : List + { + } + + public class CommandManager : ICommandManager + { + public CommandCollection Commands { get; } = new CommandCollection(); + } + + public class WindowManager : IWindowManager + { + public event EventHandler WindowLayoutLoaded; + + public ICommandBarManager CommandBarManager { get; } = new CommandBarManager(); + + public DockedWindow CreateDockedWindow(string key, string title, Control control, DockedPosition position) + { + WindowLayoutLoaded?.Invoke(this, EventArgs.Empty); + return new DockedWindow + { + Key = key, + Title = title, + Content = control, + Position = position, + }; + } + } + + public class CommandBarManager : ICommandBarManager + { + public event EventHandler UILoaded; + + public ToolCollection RootTools { get; } = new ToolCollection(); + + public void RaiseUiLoaded() + { + UILoaded?.Invoke(this, EventArgs.Empty); + } + } + + public class ToolEventArgs : EventArgs + { + public string Key { get; set; } + } + + public delegate void ToolEventHandler(object sender, ToolEventArgs args); + + public class ToolBase : ITool + { + public string Key { get; set; } + + public bool Checked { get; set; } + + public event EventHandler ToolClick; + + protected void RaiseToolClick() + { + ToolClick?.Invoke(this, EventArgs.Empty); + } + } + + public class ButtonTool : ToolBase + { + } + + public class StateButtonTool : ToolBase + { + } + + public class MenuTool : ToolBase + { + public bool IsContextMenu { get; set; } + + public ToolCollection Tools { get; } = new ToolCollection(); + } + + public class ToolCollection + { + private readonly Dictionary _items = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public event ToolEventHandler ToolAdded; + + public ITool this[string key] => _items.TryGetValue(key, out ITool tool) ? tool : null; + + public bool Contains(string key) + { + return _items.ContainsKey(key); + } + + public MenuTool AddMenuTool(string key, string caption, object image, string category) + { + MenuTool tool = new MenuTool { Key = key }; + _items[key] = tool; + ToolAdded?.Invoke(this, new ToolEventArgs { Key = key }); + return tool; + } + + public ButtonTool AddButtonTool(string key, string caption, object image, string category) + { + ButtonTool tool = new ButtonTool { Key = key }; + _items[key] = tool; + ToolAdded?.Invoke(this, new ToolEventArgs { Key = key }); + return tool; + } + + public ITool AddTool(string key) + { + if (_items.TryGetValue(key, out ITool existing)) + { + return existing; + } + + ButtonTool tool = new ButtonTool { Key = key }; + _items[key] = tool; + ToolAdded?.Invoke(this, new ToolEventArgs { Key = key }); + return tool; + } + } + + public static class MessageBoxEx + { + public static DialogResult Show(string text) + { + return DialogResult.OK; + } + + public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon) + { + return DialogResult.OK; + } + } + } + projectReferences: [] + - name: "Aveva.Core.Database" + rootNamespace: "" + sourceFiles: + - path: "src/Database.cs" + content: |- + using System; + using System.Collections; + using System.Collections.Generic; + using Aveva.Core.Geometry; + using Aveva.Core.Utilities.Messaging; + + namespace Aveva.Core.Database + { + public class DbAttribute + { + public DbAttribute() + { + } + + public DbAttribute(string name) + { + Name = name; + } + + public string Name { get; set; } + + public static DbAttribute GetDbAttribute(string name) + { + return new DbAttribute(name); + } + } + + public static class DbAttributeInstance + { + public static readonly DbAttribute NAME = new DbAttribute(nameof(NAME)); + public static readonly DbAttribute DESC = new DbAttribute(nameof(DESC)); + public static readonly DbAttribute BUIL = new DbAttribute(nameof(BUIL)); + public static readonly DbAttribute REV = new DbAttribute(nameof(REV)); + public static readonly DbAttribute PRES = new DbAttribute(nameof(PRES)); + public static readonly DbAttribute HREF = new DbAttribute(nameof(HREF)); + public static readonly DbAttribute LNTP = new DbAttribute(nameof(LNTP)); + public static readonly DbAttribute HDIR = new DbAttribute(nameof(HDIR)); + public static readonly DbAttribute POS = new DbAttribute(nameof(POS)); + public static readonly DbAttribute ORI = new DbAttribute(nameof(ORI)); + public static readonly DbAttribute FLNN = new DbAttribute(nameof(FLNN)); + public static readonly DbAttribute FLNM = new DbAttribute(nameof(FLNM)); + public static readonly DbAttribute AREA = new DbAttribute(nameof(AREA)); + public static readonly DbAttribute CREF = new DbAttribute(nameof(CREF)); + public static readonly DbAttribute ISNAME = new DbAttribute(nameof(ISNAME)); + public static readonly DbAttribute MEMB = new DbAttribute(nameof(MEMB)); + public static readonly DbAttribute DIAM = new DbAttribute(nameof(DIAM)); + public static readonly DbAttribute LCLM = new DbAttribute(nameof(LCLM)); + public static readonly DbAttribute LCLMH = new DbAttribute(nameof(LCLMH)); + public static readonly DbAttribute XLEN = new DbAttribute(nameof(XLEN)); + public static readonly DbAttribute YLEN = new DbAttribute(nameof(YLEN)); + public static readonly DbAttribute ZLEN = new DbAttribute(nameof(ZLEN)); + } + + public class DbElementType + { + public DbElementType() + { + } + + public DbElementType(string name) + { + Name = name; + } + + public string Name { get; set; } + + public static DbElementType GetElementType(int hash) + { + return new DbElementType(hash.ToString()); + } + + public static DbElementType GetElementType(string name) + { + return new DbElementType(name); + } + + public DbAttribute[] SystemAttributes() + { + return Array.Empty(); + } + } + + public static class DbElementTypeInstance + { + public static readonly DbElementType SITE = new DbElementType(nameof(SITE)); + public static readonly DbElementType ZONE = new DbElementType(nameof(ZONE)); + public static readonly DbElementType EQUIPMENT = new DbElementType(nameof(EQUIPMENT)); + public static readonly DbElementType CYLINDER = new DbElementType(nameof(CYLINDER)); + public static readonly DbElementType BOX = new DbElementType(nameof(BOX)); + public static readonly DbElementType NOZZLE = new DbElementType(nameof(NOZZLE)); + public static readonly DbElementType PIPE = new DbElementType(nameof(PIPE)); + public static readonly DbElementType BRANCH = new DbElementType(nameof(BRANCH)); + public static readonly DbElementType GASKET = new DbElementType(nameof(GASKET)); + public static readonly DbElementType ELBOW = new DbElementType(nameof(ELBOW)); + public static readonly DbElementType TEE = new DbElementType(nameof(TEE)); + } + + public class DbElement + { + public static DbElement GetElement(string name) + { + return new DbElement + { + Name = name, + IsValid = true, + }; + } + + public string Name { get; set; } + + public bool IsValid { get; set; } = true; + + public DbElement Previous { get; set; } = new DbElement(false); + + public DbElement() + { + } + + private DbElement(bool valid) + { + IsValid = valid; + } + + public void Delete() + { + } + + public DbElement Create(int position, DbElementType elementType) + { + return new DbElement(); + } + + public DbElement CreateAfter(DbElementType elementType) + { + return new DbElement(); + } + + public DbElement CreateFirst(DbElementType elementType) + { + return new DbElement(); + } + + public DbElement CreateLast(DbElementType elementType) + { + return new DbElement(); + } + + public void SetAttribute(DbAttribute attribute, object value) + { + } + + public string GetString(DbAttribute attribute) + { + return string.Empty; + } + + public bool GetBool(DbAttribute attribute) + { + return false; + } + + public int GetInteger(DbAttribute attribute) + { + return 0; + } + + public double GetDouble(DbAttribute attribute) + { + return 0d; + } + + public DbElement GetElement(DbAttribute attribute) + { + return new DbElement(); + } + + public Direction GetDirection(DbAttribute attribute) + { + return new Direction(); + } + + public Position GetPosition(DbAttribute attribute) + { + return Position.Create(0d, 0d, 0d); + } + + public Orientation GetOrientation(DbAttribute attribute) + { + return new Orientation(); + } + + public string GetAsString(DbAttribute attribute) + { + return string.Empty; + } + + public DbAttribute[] GetAttributes() + { + return Array.Empty(); + } + + public void Copy(DbElement element) + { + } + + public void CopyHierarchy(DbElement element, DbCopyOption options) + { + } + + public DbElement FirstMember() + { + return new DbElement(); + } + + public DbElement FirstMember(DbElementType type) + { + return new DbElement(); + } + + public void InsertAfterLast(DbElement element) + { + } + + public void InsertAfter(DbElement element) + { + } + + public DbElement Next() + { + return new DbElement(); + } + + public DbElement Next(DbElementType type) + { + return new DbElement(); + } + + public DbElement[] Members() + { + return Array.Empty(); + } + + public DbElement[] Members(DbElementType type) + { + return Array.Empty(); + } + + public DbElement Member(int index) + { + return new DbElement(); + } + + public DbElement[] GetElementArray(DbAttribute attribute, DbElementType type) + { + return Array.Empty(); + } + + public double EvaluateDouble(DbExpression expression, DbAttributeUnit unit) + { + return 0d; + } + + public void SetRule(DbAttribute attribute, DbRule rule) + { + } + + public DbRule GetRule(DbAttribute attribute) + { + return new DbRule(); + } + + public void DeleteRule(DbAttribute attribute) + { + } + + public bool ExistRule(DbAttribute attribute) + { + return false; + } + + public bool VerifyRule(DbAttribute attribute) + { + return false; + } + + public void ExecuteRule(DbAttribute attribute) + { + } + + public void ExecuteAllRules() + { + } + + public void PropagateRules(DbAttribute attribute) + { + } + + public void Claim() + { + } + + public void Release() + { + } + + public void ClaimHierarchy() + { + } + + public void ReleaseHierarchy() + { + } + + public DbElementType GetElementType() + { + return new DbElementType(); + } + + public void ChangeType(DbElementType type) + { + } + + public bool AtDefault(DbAttribute attribute) + { + return true; + } + + public bool IsDescendant(DbElement element) + { + return false; + } + + public void SetAttributeDefault(DbAttribute attribute) + { + } + } + + public class DBElementCollection : IEnumerable + { + public DBElementCollection(DbElement root) + { + } + + public DBElementCollection(DbElement root, object filter) + { + } + + public bool IncludeRoot { get; set; } + + public object Filter { get; set; } + + public DBElementEnumerator GetEnumerator() + { + return new DBElementEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + public class DBElementEnumerator : IEnumerator + { + public DbElement Current => new DbElement(); + + object IEnumerator.Current => Current; + + public bool MoveNext() + { + return false; + } + + public void Reset() + { + } + + public void Dispose() + { + } + } + + public class DbCopyOption + { + public string FromName { get; set; } + + public string ToName { get; set; } + + public bool Rename { get; set; } + } + + public class DbExpression + { + public static DbExpression Parse(string expression) + { + return new DbExpression(); + } + } + + public enum DbAttributeUnit + { + DIST, + } + + public enum DbRuleStatus + { + DYNAMIC, + } + + public enum DbExpressionType + { + REAL, + } + + public class DbRule + { + public static DbRule CreateDbRule(DbExpression expression, DbRuleStatus status, DbExpressionType expressionType) + { + return new DbRule(); + } + } + + public class Project + { + public static Project CurrentProject { get; } = new Project(); + + public string Name { get; set; } = string.Empty; + + public string UserName { get; set; } = string.Empty; + + public bool IsOpen() + { + return false; + } + + public void Open(string project, string username, string password) + { + Name = project; + UserName = username; + } + + public void Close() + { + } + } + + public class MDB + { + public static MDB CurrentMDB { get; } = new MDB(); + + public string Name { get; set; } = string.Empty; + + public void SaveWork(string description) + { + } + + public void CloseMDB() + { + } + + public void Open(MDBSetup setup, out PdmsMessage error) + { + error = new PdmsMessage(); + } + + public Db[] GetDBArray(DbType type) + { + return Array.Empty(); + } + + public DbElement GetFirstWorld(DbType type) + { + return new DbElement(); + } + + public void QuitWork() + { + } + } + + public class MDBSetup + { + public static MDBSetup CreateMDBSetup(string name, out PdmsMessage error) + { + error = new PdmsMessage(); + return new MDBSetup(); + } + } + + public class Db + { + public string Name { get; set; } = string.Empty; + } + + public enum DbType + { + Design, + } + + public class NameTable : IEnumerable + { + public static NameTable GetNameTable(Db db, DbAttribute attribute, string prefix, string suffix) + { + return new NameTable(); + } + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)Array.Empty()).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + public class ElementTreeNavigator + { + public ElementTreeNavigator(DbElement root, object filter) + { + } + + public DbElement FirstMemberInScan(DbElement element) + { + return new DbElement(); + } + + public DbElement NextInScan(DbElement element) + { + return new DbElement(); + } + + public DbElement Parent(DbElement element) + { + return new DbElement(); + } + + public DbElement[] MembersInScan(DbElement element) + { + return Array.Empty(); + } + } + + public class DbQualifier + { + } + + public static class DbPseudoAttribute + { + public delegate double GetDoubleDelegate(DbElement element, DbAttribute attribute, DbQualifier qualifier); + + public static void AddGetDoubleAttribute(DbAttribute attribute, DbElementType type, GetDoubleDelegate callback) + { + } + } + + public static class DbPostElementChange + { + public delegate void PostCreateDelegate(DbElement element); + + public delegate void PreDeleteDelegate(DbElement element); + + public delegate void PostMoveDelegate(DbElement element, DbElement oldOwner, int oldPosition); + + public delegate void PostAttributeChangeDelegate(DbElement element, DbAttribute attribute); + + public delegate void PostRefAttributeChangeDelegate(DbElement element, DbAttribute attribute, DbElement oldReference); + + public static void AddPostCreateElement(DbElementType type, PostCreateDelegate callback) + { + } + + public static void AddPreDeleteElement(DbElementType type, PreDeleteDelegate callback) + { + } + + public static void AddPostMoveElement(DbElementType type, PostMoveDelegate callback) + { + } + + public static void AddPostAttributeChange(DbAttribute attribute, PostAttributeChangeDelegate callback) + { + } + + public static void AddPostRefAttributeChange(DbAttribute attribute, PostRefAttributeChangeDelegate callback) + { + } + } + + public class Spatial + { + public static Spatial Instance { get; } = new Spatial(); + + public DbElement[] ElementsInBox(LimitsBox limitsBox, bool fullyWithin) + { + return Array.Empty(); + } + + public DbElement[] ElementsInElementBox(DbElement element, bool fullyWithin) + { + return Array.Empty(); + } + + public bool ElementInBox(DbElement element, LimitsBox limitsBox) + { + return false; + } + + public bool ElementInElementBox(DbElement first, DbElement second) + { + return false; + } + } + } + projectReferences: + - "Aveva.Core.Geometry" + - "Aveva.Core.Utilities" + - name: "Aveva.Core.Database.Filters" + rootNamespace: "" + sourceFiles: + - path: "src/Filters.cs" + content: |- + using Aveva.Core.Database; + + namespace Aveva.Core.Database.Filters + { + public class TypeFilter + { + public TypeFilter() + { + } + + public TypeFilter(DbElementType type) + { + } + + public void Add(DbElementType type) + { + } + + public DbElement FirstMember(DbElement element) + { + return new DbElement(); + } + + public DbElement Next(DbElement element) + { + return new DbElement(); + } + + public DbElement[] Members(DbElement element) + { + return System.Array.Empty(); + } + + public DbElement Parent(DbElement element) + { + return new DbElement(); + } + } + + public class CompoundFilter + { + public void AddShow(object filter) + { + } + } + + public class AndFilter : CompoundFilter + { + public void Add(object filter) + { + } + + public bool Valid(DbElement element) + { + return true; + } + } + + public class TrueFilter + { + } + + public class AttributeTrueFilter + { + public AttributeTrueFilter(DbAttribute attribute) + { + } + + public bool Valid(DbElement element) + { + return true; + } + } + + public class BelowOrAtType + { + public BelowOrAtType(DbElementType type) + { + } + + public bool ScanBelow(DbElement element) + { + return true; + } + } + } + projectReferences: + - "Aveva.Core.Database" + - name: "Aveva.Core.FlexibleExplorer.Control" + rootNamespace: "" + sourceFiles: + - path: "src/FlexibleExplorer.cs" + content: |- + using System.Windows.Forms; + using Aveva.ApplicationFramework; + using Aveva.Core.Presentation; + + namespace Aveva.Core.FlexibleExplorer + { + public class FlexibleExplorer : Control + { + public FlexibleExplorer(IDependencyResolver dependencyResolver, object viewContext, bool useDefaultImages) + { + } + + public BorderStyle BorderStyle { get; set; } + + public bool DisplayCheckBox { get; set; } + + public bool DisplayLockIndicator { get; set; } + + public bool DisplayClaimIndicator { get; set; } + + public bool DisplayReadOnlyIndicator { get; set; } + + public Aveva.Core.FlexibleExplorer.IFlexibleExplorerRefreshManager CustomRefreshManager { get; set; } + + public void MultiSelect(bool enabled) + { + } + + public void CreateRoots(object rootDefinitions, bool preserveExisting) + { + } + } + } + projectReferences: + - "Aveva.ApplicationFramework" + - "Aveva.Core.FlexibleExplorer.Core" + - "Aveva.Core.Presentation" + - name: "Aveva.Core.FlexibleExplorer.Core" + rootNamespace: "" + sourceFiles: + - path: "src/FlexibleExplorerCore.cs" + content: |- + using System.IO; + using Aveva.Core.Presentation; + + namespace Aveva.Core.FlexibleExplorer + { + public enum RefreshOperation + { + None, + NodesOnly, + NodesWithCollections, + WholeTree, + } + + public class RefreshArguments + { + public DbChangesEventArgs DbChangesEventArgs { get; set; } + + public object DbRawChanges { get; set; } + + public RefreshOperation RefreshType { get; set; } + } + + public interface IFlexibleExplorerRefreshManager + { + RefreshArguments GetRefreshType(RefreshArguments refreshArguments); + } + + public class XmlDefinitionProvider + { + public XmlDefinitionProvider(string xmlContent, string fileName) + { + } + + public object RootDefinitions { get; } = new object[0]; + + public static string GetLocalFile(string path) + { + return File.Exists(path) ? path : null; + } + } + } + projectReferences: + - "Aveva.Core.Presentation" + - name: "Aveva.Core.Geometry" + rootNamespace: "" + sourceFiles: + - path: "src/Geometry.cs" + content: |- + namespace Aveva.Core.Geometry + { + public class Direction + { + public static Direction Create(double x, double y, double z) + { + return new Direction(); + } + } + + public class Position + { + public double X { get; set; } + + public double Y { get; set; } + + public double Z { get; set; } + + public static Position Create(double x, double y, double z) + { + return new Position + { + X = x, + Y = y, + Z = z, + }; + } + } + + public class Orientation + { + public static Orientation Create(Direction xAxis, Direction yAxis) + { + return new Orientation(); + } + } + + public class LimitsBox + { + public static LimitsBox Create(Position first, Position second) + { + return new LimitsBox(); + } + } + } + projectReferences: [] + - name: "Aveva.Core.Implementation" + rootNamespace: "" + sourceFiles: [] + projectReferences: [] + - name: "Aveva.Core.Presentation" + rootNamespace: "" + sourceFiles: + - path: "src/Presentation.cs" + content: |- + using System; + using System.Collections; + using System.Windows.Forms; + using Aveva.ApplicationFramework.Presentation; + using Aveva.Core.Database; + using Aveva.Core.PMLNet; + + namespace Aveva.Core.Presentation + { + public delegate void CurrentElementChangedEventHandler(object sender, CurrentElementChangedEventArgs e); + + public class CurrentElementChangedEventArgs : EventArgs + { + public DbElement Element { get; set; } = new DbElement(); + } + + public static class CurrentElement + { + private static DbElement s_element = new DbElement(); + + public static event CurrentElementChangedEventHandler CurrentElementChanged; + + public static DbElement Element + { + get => s_element; + set + { + s_element = value; + CurrentElementChanged?.Invoke(null, new CurrentElementChangedEventArgs { Element = value }); + } + } + } + + public delegate void DbChangesEventHandler(object sender, DbChangesEventArgs e); + + public class DbChangesEventArgs : EventArgs + { + public ChangeList ChangeList { get; } = new ChangeList(); + } + + public class ChangeList + { + public DbElement[] GetCreations() + { + return Array.Empty(); + } + + public DbElement[] GetDeletions() + { + return Array.Empty(); + } + + public DbElement[] GetMemberChanges() + { + return Array.Empty(); + } + + public DbElement[] GetModified() + { + return Array.Empty(); + } + } + + public static class DatabaseService + { + public static event DbChangesEventHandler Changes; + + public static void RaiseChanges() + { + Changes?.Invoke(null, new DbChangesEventArgs()); + } + } + + public class NetDataSource + { + public NetDataSource(string tableName, Hashtable attributes, Hashtable titles, Hashtable items) + { + } + } + + public class NetGridControl : Control + { + public bool allGridEvents { get; set; } + + public bool ColumnExcelFilter { get; set; } + + public bool ColumnSummaries { get; set; } + + public bool EditableGrid { get; set; } + + public bool ErrorIcon { get; set; } + + public bool ExtendLastColumn { get; set; } + + public bool FixedHeaders { get; set; } + + public bool FixedRows { get; set; } + + public int GridHeight { get; set; } + + public bool HeaderSort { get; set; } + + public bool HideGroupByBox { get; set; } + + public bool OutlookGroupStyle { get; set; } + + public bool RowAddDeleteGrid { get; set; } + + public bool ScrollSelectedRowToView { get; set; } + + public bool SingleRowSelection { get; set; } + + public bool Updates { get; set; } + + public MenuTool PopupMenu { get; set; } + + public MenuTool PopupMenuHeader { get; set; } + + public event PMLNetDelegate.PMLNetEventHandler AfterSelectChange; + + public void BindToDataSource(NetDataSource dataSource) + { + } + + public void setNameColumnImage() + { + } + + public void saveGridToExcel(string path) + { + } + + public void printPreview() + { + } + + public void setRowColor(double row, string colour) + { + } + } + } + projectReferences: + - "Aveva.ApplicationFramework.Presentation" + - "Aveva.Core.Database" + - "PMLNet" + - name: "Aveva.Core.Utilities" + rootNamespace: "" + sourceFiles: + - path: "src/Utilities.cs" + content: |- + using System; + + namespace Aveva.Core.Utilities.AvevaConnect + { + public interface IAvevaConnectAuthenticationManager + { + } + + public static class CoreConnectManager + { + public static string GetAccessToken() + { + return string.Empty; + } + } + } + + namespace Aveva.Core.Utilities.Messaging + { + public class PdmsMessage + { + } + + public class PdmsError + { + public string MessageText() + { + return string.Empty; + } + } + + public class PdmsException : Exception + { + public PdmsError Error { get; } = new PdmsError(); + + public PdmsException() + { + } + + public PdmsException(string message) : base(message) + { + } + } + } + + namespace Aveva.Core.Utilities.Undo + { + public class UndoTransaction + { + public static UndoTransaction GetUndoTransaction() + { + return new UndoTransaction(); + } + + public void StartTransaction(string description) + { + } + + public void EndTransaction() + { + } + + public static void PerformUndo() + { + } + + public static void PerformRedo() + { + } + } + + public abstract class UndoSubscriber + { + public virtual object GetBeginState() + { + return null; + } + + public virtual object GetEndState() + { + return null; + } + + public virtual void RestoreBeginState(object state) + { + } + + public virtual void RestoreEndState(object state) + { + } + } + + public static class UndoCaretaker + { + public static void RegisterUndoSubscriber(UndoSubscriber subscriber) + { + } + + public static void RemoveUndoSubscriber(UndoSubscriber subscriber) + { + } + } + } + + namespace Aveva.Core.Utilities.CommandLine + { + public static class Command + { + public static void Update() + { + } + } + } + projectReferences: [] + - name: "Aveva.Core3D.Clasher" + rootNamespace: "" + sourceFiles: + - path: "src/Clasher.cs" + content: |- + using System; + using Aveva.Core.Database; + using Aveva.Core.Geometry; + + namespace Aveva.Core3D.Clasher + { + public enum SpatialMapStatus + { + Unknown, + Complete, + } + + public class SpatialMap + { + public static SpatialMap Instance { get; } = new SpatialMap(); + + public SpatialMapStatus CheckSpatialMap(Db db) + { + return SpatialMapStatus.Complete; + } + + public void BuildSpatialMap() + { + } + } + + public class ClashOptions + { + public bool IncludeTouches { get; set; } + + public double Clearance { get; set; } + + public static ClashOptions Create() + { + return new ClashOptions(); + } + } + + public class ObstructionList + { + public static ObstructionList Create() + { + return new ObstructionList(); + } + } + + public class ClashSet + { + public Clash[] Clashes { get; set; } = Array.Empty(); + + public static ClashSet Create() + { + return new ClashSet(); + } + } + + public class Clasher + { + public static Clasher Instance { get; } = new Clasher(); + + public bool CheckAll(ClashOptions options, ObstructionList obstructions, ClashSet clashSet) + { + return false; + } + } + + public class Clash + { + public DbElement First { get; set; } = new DbElement(); + + public DbElement Second { get; set; } = new DbElement(); + + public ClashType Type { get; set; } + + public Position ClashPosition { get; set; } = Position.Create(0d, 0d, 0d); + } + + public enum ClashType + { + None, + } + } + projectReferences: + - "Aveva.Core.Database" + - "Aveva.Core.Geometry" + - name: "GridControl" + rootNamespace: "" + sourceFiles: [] + projectReferences: [] + - name: "Microsoft.Web.WebView2.Core" + rootNamespace: "" + sourceFiles: + - path: "src/WebView2Core.cs" + content: |- + using System; + + namespace Microsoft.Web.WebView2.Core + { + public class CoreWebView2Settings + { + public bool AreDefaultContextMenusEnabled { get; set; } + + public bool IsStatusBarEnabled { get; set; } + + public bool IsZoomControlEnabled { get; set; } + + public bool AreDevToolsEnabled { get; set; } + } + + public class CoreWebView2NavigationStartingEventArgs : EventArgs + { + } + + public class CoreWebView2NavigationCompletedEventArgs : EventArgs + { + } + + public class CoreWebView2 + { + public CoreWebView2Settings Settings { get; } = new CoreWebView2Settings(); + + public event EventHandler NavigationStarting; + + public event EventHandler NavigationCompleted; + } + } + projectReferences: [] + - name: "Microsoft.Web.WebView2.WinForms" + rootNamespace: "" + sourceFiles: + - path: "src/WebView2.cs" + content: |- + using System; + using System.ComponentModel; + using System.Threading.Tasks; + using System.Windows.Forms; + + namespace Microsoft.Web.WebView2.WinForms + { + public class WebView2 : Control, ISupportInitialize + { + public Microsoft.Web.WebView2.Core.CoreWebView2 CoreWebView2 { get; private set; } = new Microsoft.Web.WebView2.Core.CoreWebView2(); + + public double ZoomFactor { get; set; } + + public Uri Source { get; set; } + + public Task EnsureCoreWebView2Async(object environment) + { + if (CoreWebView2 == null) + { + CoreWebView2 = new Microsoft.Web.WebView2.Core.CoreWebView2(); + } + + return Task.CompletedTask; + } + + public void BeginInit() + { + } + + public void EndInit() + { + } + } + } + projectReferences: + - "Microsoft.Web.WebView2.Core" + - name: "Newtonsoft.Json" + rootNamespace: "" + sourceFiles: + - path: "src/Json.cs" + content: |- + using System.Collections; + using System.Collections.Generic; + + namespace Newtonsoft.Json.Linq + { + public enum JTokenType + { + Object, + Array, + String, + Integer, + Float, + Boolean, + Null, + } + + public abstract class JToken + { + public abstract JTokenType Type { get; } + + public static JToken Parse(string json) + { + return new JObject(); + } + + public virtual T Value() + { + return default(T); + } + + public override string ToString() + { + return string.Empty; + } + } + + public sealed class JObject : JToken + { + public override JTokenType Type => JTokenType.Object; + + public IEnumerable Properties() + { + return new JProperty[0]; + } + } + + public sealed class JArray : JToken, IEnumerable + { + public override JTokenType Type => JTokenType.Array; + + public int Count => 0; + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)new JToken[0]).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + public sealed class JProperty + { + public string Name { get; set; } = string.Empty; + + public JToken Value { get; set; } = new JValue(); + } + + public sealed class JValue : JToken + { + public override JTokenType Type => JTokenType.String; + + public override T Value() + { + return default(T); + } + } + } + projectReferences: [] + - name: "PMLNet" + rootNamespace: "Aveva.Core.PMLNet" + sourceFiles: + - path: "src/PmlNet.cs" + content: |- + using System; + using System.Collections; + + namespace Aveva.Core.PMLNet + { + [AttributeUsage(AttributeTargets.All, AllowMultiple = false)] + public sealed class PMLNetCallableAttribute : Attribute + { + } + + public static class PMLNetDelegate + { + public delegate void PMLNetEventHandler(ArrayList args); + } + } + projectReferences: [] + - name: "StartUp" + rootNamespace: "" + sourceFiles: [] + projectReferences: [] + +steps: + - pwsh: | # Generate shim manifest + $outputPath = '${{ parameters.OutputPath }}' + $outputDirectory = Split-Path -Path $outputPath -Parent + if (-not [string]::IsNullOrWhiteSpace($outputDirectory)) { + $null = New-Item -ItemType Directory -Path $outputDirectory -Force + } + + $shimDefinitions = $env:SHIMS_JSON | ConvertFrom-Json + $projects = [ordered]@{} + foreach ($shim in @($shimDefinitions)) { + $project = [ordered]@{} + + $sourceFiles = [System.Collections.Generic.List[object]]::new() + foreach ($sourceFile in @($shim.sourceFiles)) { + if ($null -eq $sourceFile) { + continue + } + + $sourcePath = [string]$sourceFile.path + $sourceContent = [string]$sourceFile.content + if ([string]::IsNullOrWhiteSpace($sourcePath)) { + continue + } + + $null = $sourceFiles.Add([ordered]@{ + Path = $sourcePath + Content = $sourceContent + }) + } + + if ($sourceFiles.Count -gt 0) { + $project['SourceFiles'] = @($sourceFiles) + } + + $projectReferences = @($shim.projectReferences | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }) + if ($projectReferences.Count -gt 0) { + $project['ProjectReferences'] = @($projectReferences | ForEach-Object { [string]$_ }) + } + + $rootNamespace = [string]$shim.rootNamespace + if (-not [string]::IsNullOrWhiteSpace($rootNamespace)) { + $project['RootNamespace'] = $rootNamespace + } + + $projects[[string]$shim.name] = $project + } + + $manifest = @{ + Projects = $projects + } + + $manifest | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $outputPath -Encoding utf8 + Write-Host "Generated shim manifest: $outputPath" + displayName: Generate shim manifest + env: + SHIMS_JSON: ${{ convertToJson(parameters.Shims) }} diff --git a/templates/FlexibleExplorerAddInTemplate/FlexibleExplorerAddInTemplate.cs b/templates/FlexibleExplorerAddInTemplate/FlexibleExplorerAddInTemplate.cs index a4f350a..b100b17 100644 --- a/templates/FlexibleExplorerAddInTemplate/FlexibleExplorerAddInTemplate.cs +++ b/templates/FlexibleExplorerAddInTemplate/FlexibleExplorerAddInTemplate.cs @@ -2,6 +2,7 @@ using Aveva.ApplicationFramework; using Aveva.ApplicationFramework.Presentation; using Aveva.Core.Database; +using Aveva.Core.Presentation; namespace Aveva.Core.Samples { diff --git a/templates/FlexibleExplorerAddInTemplate/MyFlexibleExplorerControl.cs b/templates/FlexibleExplorerAddInTemplate/MyFlexibleExplorerControl.cs index 6124ce1..f491d80 100644 --- a/templates/FlexibleExplorerAddInTemplate/MyFlexibleExplorerControl.cs +++ b/templates/FlexibleExplorerAddInTemplate/MyFlexibleExplorerControl.cs @@ -17,7 +17,7 @@ public partial class MyFlexibleExplorerControl : UserControl /// /// The flexible explorer. /// - private FlexibleExplorer.FlexibleExplorer _flexibleExplorer; + private global::Aveva.Core.FlexibleExplorer.FlexibleExplorer _flexibleExplorer; /// /// The dependency resolver. @@ -134,7 +134,7 @@ private string ValidatePath(string filePath, string baseDirectory) /// /// A FlexibleExplorer. /// - private FlexibleExplorer.FlexibleExplorer InitialiseFlexibleExplorer(string xmlFileName) + private global::Aveva.Core.FlexibleExplorer.FlexibleExplorer InitialiseFlexibleExplorer(string xmlFileName) { if (_flexibleExplorer != null) { @@ -142,7 +142,7 @@ private FlexibleExplorer.FlexibleExplorer InitialiseFlexibleExplorer(string xmlF } else { - _flexibleExplorer = new FlexibleExplorer.FlexibleExplorer(_dependencyResolver, null, true); + _flexibleExplorer = new global::Aveva.Core.FlexibleExplorer.FlexibleExplorer(_dependencyResolver, null, true); _flexibleExplorer.Dock = DockStyle.Fill; _flexibleExplorer.BorderStyle = BorderStyle.FixedSingle; @@ -185,37 +185,6 @@ private FlexibleExplorer.FlexibleExplorer InitialiseFlexibleExplorer(string xmlF _flexibleExplorer.CreateRoots(definitionProvider.RootDefinitions, false); } catch (SecurityException ex) - { - // Handle path traversal attempts - Trace.TraceWarning("[SECURITY] Security exception loading XML '{0}': {1}", xmlFileName, ex.Message); - Log.Logger().MessageFormat("Security error loading {0}: {1}", xmlFileName, ex.Message); - ApplicationFramework.Presentation.MessageBoxEx.Show( - $"Security policy violation: {ex.Message}", - "Security Error", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - } - catch (XmlException ex) - { - // Handle XML parsing errors - Log.Logger().MessageFormat("XML parsing error in {0}: {1}", xmlFileName, ex.Message); - ApplicationFramework.Presentation.MessageBoxEx.Show( - $"Failed to load XML configuration: {ex.Message}", - "Configuration Error", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - } - catch (FileNotFoundException ex) - { - // Handle missing file errors - Log.Logger().MessageFormat("File not found: {0}", ex.Message); - ApplicationFramework.Presentation.MessageBoxEx.Show( - $"Configuration file not found: {ex.Message}", - "File Not Found", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - } - catch (SecurityException ex) { // Security-related initialization failure Trace.TraceWarning("[SECURITY] Security exception during Flexible Explorer initialization: {0}", ex.GetType().Name); diff --git a/templates/WebpageAddInTemplate/WebpageAddInTemplate.csproj b/templates/WebpageAddInTemplate/WebpageAddInTemplate.csproj index 63fb234..6821ad3 100644 --- a/templates/WebpageAddInTemplate/WebpageAddInTemplate.csproj +++ b/templates/WebpageAddInTemplate/WebpageAddInTemplate.csproj @@ -57,6 +57,7 @@ +