Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ The installer places a `.addin` manifest file in the Revit Addins folder, instru

### pyRevitLoader.dll
The loader dll is the C# entry point for pyRevit inside Revit. There are multiple versions to support:
- Different Revit versions: one for Revit 2025 and newer, another for older versions.
- Different Revit versions: one for Revit 2025 and newer, another for Revit 2021-2024.
- Different IronPython versions: 2.7.12 (default) and 3.4.0 (available but not fully tested).

> Since only one IronPython engine can be active at a time, pyRevit updates the `.addin` manifest to point to the correct loader when the user switches engines. If installation issues arise, running `pyrevit attach` usually resolves them by regenerating the manifest correctly.
Expand Down
50 changes: 26 additions & 24 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,29 @@ pyRevit is a Rapid Application Development (RAD) environment for Autodesk Revit.

## Build Commands

All build commands use pipenv. Run from the repository root:
The build is driven by the C# ModularPipelines project under [`build/`](build/). Run from `build/`:

```bash
# Setup environment
pipenv install
pipenv run pyrevit check # Verify build environment

# Build commands
pipenv run pyrevit build products # Build all C# DLLs (Release mode)
pipenv run pyrevit build products Debug # Build in Debug mode
pipenv run pyrevit build labs # Build main project only
pipenv run pyrevit build engines # Build Python engines
pipenv run pyrevit build installers # Create Inno Setup installers

# Cleaning
pipenv run pyrevit clean labs # Clean build artifacts

# Version management
pipenv run pyrevit set version <ver> # Set version number
pipenv run pyrevit set build wip # Set as work-in-progress
pipenv run pyrevit set build release # Set as release build
```powershell
# Default unsigned local build (Channel=none)
dotnet run -c Release -- ci

# Debug build (attach the Visual Studio debugger to revit.exe)
dotnet run -c Debug -- ci

# WIP-style stamping + product build (mirrors develop push on the main repo)
$env:Build__Channel = 'wip'
dotnet run -c Release -- ci

# Release-style stamping + product build (mirrors master / tag CI on the main repo)
$env:Build__Channel = 'release'
$env:DOTNET_ENVIRONMENT = 'Production'
dotnet run -c Release -- ci
```

Other pipeline modes: `pack`, `sign`, `publish`, `winget`, `notify`. See [`build/README.md`](build/README.md) for the full list, `Build__Channel` semantics, and CI gating.

The legacy Python CLI (`pipenv run pyrevit build products`, `set build`, `set products`, …) still works for local/manual workflows and is documented in [`docs/ci-cd.md`](docs/ci-cd.md); CI no longer invokes it.

## Documentation

- Main website: https://pyrevitlabs.io/
Expand All @@ -74,7 +74,7 @@ pyrevit attach dev default --installed
2. Checkout `develop` branch (active development)
3. Initialize submodules: `git submodule update --init --recursive`
4. Install dependencies: `pipenv install`
5. Build: `pipenv run pyrevit build products Debug`
5. Build: `cd build && dotnet run -c Debug -- ci && cd ..`
6. Test in Revit by attaching the clone

For debugging C# code:
Expand All @@ -87,9 +87,11 @@ For debugging C# code:
### Loading Sequence
1. Revit reads `.addin` manifest from Addins folder
2. Manifest points to `pyRevitLoader.dll` (C#)
3. Loader launches `pyrevitloader.py` in IronPython
4. Python script calls `pyrevit.loader.sessionmgr.load_session()`
5. Extensions are discovered and UI is built
3. `PyRevitLoaderApplication.OnStartup` calls the C# session manager directly (no IronPython bootstrap)
4. `SessionManagerService.LoadSession` runs `session_preload.py`, builds extension assemblies and UI in C#, then runs `session_postload.py`
5. The preload/postload scripts drive the residual Python session services (telemetry, routes, output window, hooks framework) through the runtime engine

Reload re-enters the same C# orchestrator via `PyRevitLoaderApplication.LoadSession`. The legacy pure-Python loader has been removed; the C# loader requires Revit 2021+.

### Key Components
- **pyRevitLoader** (`dev/pyRevitLoader/`): Revit add-in entry point
Expand Down
28 changes: 2 additions & 26 deletions dev/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -29,36 +29,12 @@
</PropertyGroup>

<PropertyGroup>
<RevitVersion Condition="'$(RevitVersion)' == '' and '$(TargetFramework)' == 'net48'">2017</RevitVersion>
<RevitVersion Condition="'$(RevitVersion)' == '' and '$(TargetFramework)' == 'net48'">2021</RevitVersion>
<RevitVersion Condition="'$(RevitVersion)' == '' and '$(TargetFramework)' == 'net8.0-windows'">2025</RevitVersion>
<RevitVersion Condition="'$(RevitVersion)' == '' and '$(TargetFramework)' == 'net10.0-windows'">2027</RevitVersion>
</PropertyGroup>

<!-- Define REVIT version constants based on RevitVersion (must be in targets, not props, to evaluate after project sets RevitVersion) -->
<PropertyGroup Condition="'$(RevitVersion)' == '2013'">
<DefineConstants>$(DefineConstants);REVIT2013</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(RevitVersion)' == '2014'">
<DefineConstants>$(DefineConstants);REVIT2014</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(RevitVersion)' == '2015'">
<DefineConstants>$(DefineConstants);REVIT2015</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(RevitVersion)' == '2016'">
<DefineConstants>$(DefineConstants);REVIT2016</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(RevitVersion)' == '2017'">
<DefineConstants>$(DefineConstants);REVIT2017</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(RevitVersion)' == '2018'">
<DefineConstants>$(DefineConstants);REVIT2018</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(RevitVersion)' == '2019'">
<DefineConstants>$(DefineConstants);REVIT2019</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(RevitVersion)' == '2020'">
<DefineConstants>$(DefineConstants);REVIT2020</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(RevitVersion)' == '2021'">
<DefineConstants>$(DefineConstants);REVIT2021;REVIT2021_OR_GREATER</DefineConstants>
</PropertyGroup>
Expand Down Expand Up @@ -89,7 +65,7 @@
<!-- Xceed.Wpf.AvalonDock reference (must be in targets to use RevitDevDir and RevitVersion) -->
<ItemGroup Condition="'$(UseRevit)' == 'true'">
<Reference
Condition="'$(RevitVersion)' != '' and $(RevitVersion) &gt;= 2018"
Condition="'$(RevitVersion)' != '' and $(RevitVersion) &gt;= 2021"
Include="Xceed.Wpf.AvalonDock"
HintPath="$(RevitDevDir)\$(RevitVersion)\Xceed.Wpf.AvalonDock.dll"/>
</ItemGroup>
Expand Down
Binary file removed dev/libs/Revit/2018/Xceed.Wpf.AvalonDock.dll
Binary file not shown.
Binary file removed dev/libs/Revit/2019/Xceed.Wpf.AvalonDock.dll
Binary file not shown.
Binary file removed dev/libs/Revit/2020/Xceed.Wpf.AvalonDock.dll
Binary file not shown.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

40 changes: 0 additions & 40 deletions dev/pyRevitLabs.PyRevit.Runtime/pyRevitLabs.PyRevit.Runtime.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32421.90
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "pyRevitLabs.PyRevit.Runtime.2017", "2017\pyRevitLabs.PyRevit.Runtime.2017.csproj", "{1B786F43-6125-42FF-8EA2-C4AEF640AEE4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "pyRevitLabs.PyRevit.Runtime.2018", "2018\pyRevitLabs.PyRevit.Runtime.2018.csproj", "{8CEA0B80-D74F-411F-A607-DC1599AA934E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "pyRevitLabs.PyRevit.Runtime.2019", "2019\pyRevitLabs.PyRevit.Runtime.2019.csproj", "{EFBE5B7E-227A-4122-98D7-D1F353AAF157}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "pyRevitLabs.PyRevit.Runtime.2020", "2020\pyRevitLabs.PyRevit.Runtime.2020.csproj", "{F5A23012-880A-4FE6-89EC-6F9DAB148DEF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "pyRevitLabs.PyRevit.Runtime.2021", "2021\pyRevitLabs.PyRevit.Runtime.2021.csproj", "{479435EA-C820-49E4-942F-1274C151EE71}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "pyRevitLabs.PyRevit.Runtime.2022", "2022\pyRevitLabs.PyRevit.Runtime.2022.csproj", "{DB535262-30DA-404D-A32E-F024FF37935C}"
Expand All @@ -33,38 +25,6 @@ Global
Release IPY342|x64 = Release IPY342|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1B786F43-6125-42FF-8EA2-C4AEF640AEE4}.Debug IPY2712PR|x64.ActiveCfg = Debug IPY2712PR|x64
{1B786F43-6125-42FF-8EA2-C4AEF640AEE4}.Debug IPY2712PR|x64.Build.0 = Debug IPY2712PR|x64
{1B786F43-6125-42FF-8EA2-C4AEF640AEE4}.Debug IPY342|x64.ActiveCfg = Debug IPY342|x64
{1B786F43-6125-42FF-8EA2-C4AEF640AEE4}.Debug IPY342|x64.Build.0 = Debug IPY342|x64
{1B786F43-6125-42FF-8EA2-C4AEF640AEE4}.Release IPY2712PR|x64.ActiveCfg = Release IPY2712PR|x64
{1B786F43-6125-42FF-8EA2-C4AEF640AEE4}.Release IPY2712PR|x64.Build.0 = Release IPY2712PR|x64
{1B786F43-6125-42FF-8EA2-C4AEF640AEE4}.Release IPY342|x64.ActiveCfg = Release IPY342|x64
{1B786F43-6125-42FF-8EA2-C4AEF640AEE4}.Release IPY342|x64.Build.0 = Release IPY342|x64
{8CEA0B80-D74F-411F-A607-DC1599AA934E}.Debug IPY2712PR|x64.ActiveCfg = Debug IPY2712PR|x64
{8CEA0B80-D74F-411F-A607-DC1599AA934E}.Debug IPY2712PR|x64.Build.0 = Debug IPY2712PR|x64
{8CEA0B80-D74F-411F-A607-DC1599AA934E}.Debug IPY342|x64.ActiveCfg = Debug IPY342|x64
{8CEA0B80-D74F-411F-A607-DC1599AA934E}.Debug IPY342|x64.Build.0 = Debug IPY342|x64
{8CEA0B80-D74F-411F-A607-DC1599AA934E}.Release IPY2712PR|x64.ActiveCfg = Release IPY2712PR|x64
{8CEA0B80-D74F-411F-A607-DC1599AA934E}.Release IPY2712PR|x64.Build.0 = Release IPY2712PR|x64
{8CEA0B80-D74F-411F-A607-DC1599AA934E}.Release IPY342|x64.ActiveCfg = Release IPY342|x64
{8CEA0B80-D74F-411F-A607-DC1599AA934E}.Release IPY342|x64.Build.0 = Release IPY342|x64
{EFBE5B7E-227A-4122-98D7-D1F353AAF157}.Debug IPY2712PR|x64.ActiveCfg = Debug IPY2712PR|x64
{EFBE5B7E-227A-4122-98D7-D1F353AAF157}.Debug IPY2712PR|x64.Build.0 = Debug IPY2712PR|x64
{EFBE5B7E-227A-4122-98D7-D1F353AAF157}.Debug IPY342|x64.ActiveCfg = Debug IPY342|x64
{EFBE5B7E-227A-4122-98D7-D1F353AAF157}.Debug IPY342|x64.Build.0 = Debug IPY342|x64
{EFBE5B7E-227A-4122-98D7-D1F353AAF157}.Release IPY2712PR|x64.ActiveCfg = Release IPY2712PR|x64
{EFBE5B7E-227A-4122-98D7-D1F353AAF157}.Release IPY2712PR|x64.Build.0 = Release IPY2712PR|x64
{EFBE5B7E-227A-4122-98D7-D1F353AAF157}.Release IPY342|x64.ActiveCfg = Release IPY342|x64
{EFBE5B7E-227A-4122-98D7-D1F353AAF157}.Release IPY342|x64.Build.0 = Release IPY342|x64
{F5A23012-880A-4FE6-89EC-6F9DAB148DEF}.Debug IPY2712PR|x64.ActiveCfg = Debug IPY2712PR|x64
{F5A23012-880A-4FE6-89EC-6F9DAB148DEF}.Debug IPY2712PR|x64.Build.0 = Debug IPY2712PR|x64
{F5A23012-880A-4FE6-89EC-6F9DAB148DEF}.Debug IPY342|x64.ActiveCfg = Debug IPY342|x64
{F5A23012-880A-4FE6-89EC-6F9DAB148DEF}.Debug IPY342|x64.Build.0 = Debug IPY342|x64
{F5A23012-880A-4FE6-89EC-6F9DAB148DEF}.Release IPY2712PR|x64.ActiveCfg = Release IPY2712PR|x64
{F5A23012-880A-4FE6-89EC-6F9DAB148DEF}.Release IPY2712PR|x64.Build.0 = Release IPY2712PR|x64
{F5A23012-880A-4FE6-89EC-6F9DAB148DEF}.Release IPY342|x64.ActiveCfg = Release IPY342|x64
{F5A23012-880A-4FE6-89EC-6F9DAB148DEF}.Release IPY342|x64.Build.0 = Release IPY342|x64
{479435EA-C820-49E4-942F-1274C151EE71}.Debug IPY2712PR|x64.ActiveCfg = Debug IPY2712PR|x64
{479435EA-C820-49E4-942F-1274C151EE71}.Debug IPY2712PR|x64.Build.0 = Debug IPY2712PR|x64
{479435EA-C820-49E4-942F-1274C151EE71}.Debug IPY342|x64.ActiveCfg = Debug IPY342|x64
Expand Down
2 changes: 0 additions & 2 deletions dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitConsts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,6 @@ public static class PyRevitConsts {
public const string BundleScriptRevitFamilyPostfix = ".rfa";

// loader settings
public const string ConfigsNewLoaderKey = "new_loader";
public const bool ConfigsNewLoaderDefault = true;
public const string ConfigsReadScriptMetadataKey = "read_script_metadata";
public const bool ConfigsReadScriptMetadataDefault = true;

Expand Down
95 changes: 34 additions & 61 deletions dev/pyRevitLoader/Source/PyRevitLoaderApplication.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
/*
██▓███▓██ ██▓ ██▀███ ▓█████ ██▒ █▓ ██▓▄▄▄█████▓
▓██░ ██▒██ ██▒▓██ ▒ ██▒▓█ ▀▓██░ █▒▓██▒▓ ██▒ ▓▒
▓██░ ██▓▒▒██ ██░▓██ ░▄█ ▒▒███ ▓██ █▒░▒██▒▒ ▓██░ ▒░
▒██▄█▓▒ ▒░ ▐██▓░▒██▀▀█▄ ▒▓█ ▄ ▒██ █░░░██░░ ▓██▓ ░
▒██▒ ░ ░░ ██▒▓░░██▓ ▒██▒░▒████▒ ▒▀█░ ░██░ ▒██▒ ░
▒▓▒░ ░ ░ ██▒▒▒ ░ ▒▓ ░▒▓░░░ ▒░ ░ ░ ▐░ ░▓ ▒ ░░
░▒ ░ ▓██ ░▒░ ░▒ ░ ▒░ ░ ░ ░ ░ ░░ ▒ ░ ░
░░ ▒ ▒ ░░ ░░ ░ ░ ░░ ▒ ░ ░
░ ░ ░ ░ ░ ░ ░
░ ░ ░

This is the entry point for pyRevit. Revit loads the PyRevitLoader.dll addon
at startup, and PyRevitLoaderApplication.OnStartup calls into the C# session
manager to build the UI and the button commands.
*/

using Autodesk.Revit.Attributes;
using Autodesk.Revit.UI;
using pyRevitAssemblyBuilder.AssemblyMaker;
Expand Down Expand Up @@ -49,17 +66,10 @@ Result IExternalApplication.OnStartup(UIControlledApplication application)
var commonFolder = Path.GetDirectoryName(Path.GetDirectoryName(LoaderPath));
LoadAssembliesInFolder(commonFolder);

try
{
var uiApplication = GetUIApplication(application);
var result = ExecuteStartupScript(application);
return result;
}
catch (Exception ex)
{
TaskDialog.Show("Error Loading Startup Script", ex.ToString());
return Result.Failed;
}
// Load the session directly through the C# session manager. The Python
// pre/post-load services are driven from within LoadSession, so Revit
// startup no longer bootstraps an IronPython engine to reach the loader.
return LoadSessionInternal(firstLoad: true);
}

private static void LoadAssembliesInFolder(string folder)
Expand All @@ -80,81 +90,44 @@ private static void LoadAssembliesInFolder(string folder)
}
}

private static Result ExecuteStartupScript(UIControlledApplication uiControlledApplication)
{
var uiApplication = GetUIApplication(uiControlledApplication);
// execute StartupScript
Result result = Result.Succeeded;
var startupScript = GetStartupScriptPath();
if (startupScript != null)
{
var executor = new ScriptExecutor(uiApplication);
result = executor.ExecuteScript(startupScript);
if (result == Result.Failed)
{
TaskDialog.Show("Error Loading pyRevit", executor.Message);
}
}

return result;
}
// Reload entry invoked by the Python session manager via reflection
// (GetMethod("LoadSession")). Must stay the only static method named LoadSession
// so that lookup remains unambiguous. A reload is never the first load.
public static Result LoadSession() => LoadSessionInternal(firstLoad: false);

public static Result LoadSession(string buildStrategy = null)
// Shared entry for initial startup and reload. The C# SessionManagerService
// drives the full load, including the residual Python pre/post-load services.
private static Result LoadSessionInternal(bool firstLoad)
{
try
{
// Use the stored UIControlledApplication
if (_uiControlledApplication == null)
{
throw new InvalidOperationException("UIControlledApplication not available." +
" LoadSession can only be called after OnStartup.");
}

var uiControlledApplication = _uiControlledApplication;
var uiApplication = GetUIApplication(uiControlledApplication);

// Get the current Revit version
var revitVersion = uiControlledApplication.ControlledApplication.VersionNumber;

// Always use Roslyn build strategy
AssemblyBuildStrategy strategyEnum = AssemblyBuildStrategy.Roslyn;
var uiApplication = GetUIApplication(_uiControlledApplication);
var revitVersion = _uiControlledApplication.ControlledApplication.VersionNumber;

var sessionManager = ServiceFactory.CreateSessionManagerService(
revitVersion,
strategyEnum,
AssemblyBuildStrategy.Roslyn,
uiApplication);

// Load the session using the C# SessionManagerService
sessionManager.LoadSession();
sessionManager.LoadSession(firstLoad);

return Result.Succeeded;
}
catch (Exception ex)
{
TaskDialog.Show("Error Loading C# Session",
$"An error occurred while loading the C# session:\n\n{ex.Message}\n\n" +
TaskDialog.Show("Error Loading pyRevit Session",
$"An error occurred while loading the pyRevit session:\n\n{ex.Message}\n\n" +
$"Check the output window for details.");
return Result.Failed;
}
}
private static string GetStartupScriptPath()
{
var assemblyLocation = Assembly.GetExecutingAssembly().Location;
var loaderDir = Path.GetDirectoryName(assemblyLocation);
if (string.IsNullOrEmpty(loaderDir))
{
throw new InvalidOperationException($"Could not determine directory for assembly location: {assemblyLocation}");
}

var dllDir = Path.GetDirectoryName(loaderDir);
if (string.IsNullOrEmpty(dllDir))
{
throw new InvalidOperationException($"Could not determine parent directory for loader directory: {loaderDir}");
}

var assemblyName = Assembly.GetExecutingAssembly().GetName().Name;
return Path.Combine(dllDir, $"{assemblyName}.py");
}
Result IExternalApplication.OnShutdown(UIControlledApplication application)
{
// FIXME: deallocate the python shell...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ internal static class Constants
/// Key for referenced assemblies in environment dictionary.
/// </summary>
public const string REFED_ASSMS_KEY = "PYREVIT_REFEDASSMS";

/// <summary>
/// Python entry script that runs pre-load session setup.
/// </summary>
public const string PRELOAD_SCRIPT = "session_preload.py";

/// <summary>
/// Python entry script that runs post-load session finalization.
/// </summary>
public const string POSTLOAD_SCRIPT = "session_postload.py";
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public interface ISessionManagerService
/// - Executes startup scripts if present
/// - Creates the UI
/// </remarks>
void LoadSession();
/// <param name="firstLoad">True on initial Revit startup, false on reload. Gates
/// first-load-only work such as removing stale appdata files from closed sessions.</param>
void LoadSession(bool firstLoad);
}
}
Loading