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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Scribe LocalDev sentinels — per-producer (.scribe.user, .hermetic.local, ...)
# Either suffix activates producer mode; both are supported (see Scribe docs).
*.user
*.local
# Scribe LocalDev sentinels — per-producer (.scribe.scribe, .hermetic.scribe, ...)
*.scribe

# Build output
bin/
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<!-- ── Scribe LocalDev ─────────────────────────────────────────────── -->
<!-- Scribe can't get these from a NuGet package (it IS the package),
so we set them here and import the props file directly.
$(ScribesName) drives sentinel detection (.scribe.user or .scribe.local),
$(ScribesName) drives sentinel detection (.scribe.scribe),
auto-pack, and override-file naming. -->
<PropertyGroup>
<ScribeRoot>$(MSBuildThisFileDirectory)..</ScribeRoot>
Expand Down
11 changes: 6 additions & 5 deletions Scribe.Sdk/Sdk/Phases/Analyzer.targets
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@
Companion + Library.
-->

<!-- This file is only imported when $(_IsScribeAnalyzer) is true (gate lives
on the <Import> in Sdk.targets). No per-node conditions needed inside. -->

<!-- ── Analyzer defaults ───────────────────────────────────────────────── -->
<PropertyGroup Condition="'$(_IsScribeAnalyzer)' == 'true'">
<PropertyGroup>
<IncludeBuildOutput>false</IncludeBuildOutput>
<PackageType>Analyzer</PackageType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>

<!-- ── Own DLL into analyzers/dotnet/cs/ ───────────────────────────────── -->
<Target Name="_ScribeSdkAddAnalyzerDll"
BeforeTargets="_GetPackageFiles"
Condition="'$(_IsScribeAnalyzer)' == 'true'">
BeforeTargets="_GetPackageFiles">
<ItemGroup>
<None Include="$(TargetPath)"
Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
Expand All @@ -31,8 +33,7 @@
<!-- ── Private dependency DLLs into analyzers/dotnet/cs/ ───────────────── -->
<Target Name="_ScribeSdkAddAnalyzerDependencies"
BeforeTargets="_GetPackageFiles"
DependsOnTargets="ResolvePackageAssets"
Condition="'$(_IsScribeAnalyzer)' == 'true'">
DependsOnTargets="ResolvePackageAssets">
<ItemGroup>
<_ScribeSdkDep Include="@(ReferenceCopyLocalPaths)"
Condition="'%(ReferenceCopyLocalPaths.NuGetPackageId)' != ''
Expand Down
8 changes: 5 additions & 3 deletions Scribe.Sdk/Sdk/Phases/Companion.targets
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@
</PropertyGroup>
-->

<!-- This file is only imported when $(_IsScribeCompanion) is true (gate lives
on the <Import> in Sdk.targets). No per-node conditions needed inside. -->

<!-- ── Companion defaults ──────────────────────────────────────────────── -->
<PropertyGroup Condition="'$(_IsScribeCompanion)' == 'true'">
<PropertyGroup>
<!-- Companion DLL lives in analyzers/dotnet/cs/ of the host's package. -->
<IncludeBuildOutput>false</IncludeBuildOutput>
<!-- Companions never produce their own nupkg. -->
Expand All @@ -44,7 +47,6 @@
conflicts at analyze time).
-->
<Target Name="GetAnalyzerFiles"
Condition="'$(_IsScribeCompanion)' == 'true'"
DependsOnTargets="ResolveAssemblyReferences"
Returns="@(_AnalyzerFile)">
<ItemGroup>
Expand All @@ -68,7 +70,7 @@
TargetPathWithTargetPlatformMoniker carries them across the reference
boundary.
-->
<Target Name="GetDependencyTargetPaths" Condition="'$(_IsScribeCompanion)' == 'true'">
<Target Name="GetDependencyTargetPaths">
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="@(ReferenceCopyLocalPaths)"
IncludeRuntimeDependency="false" />
Expand Down
7 changes: 5 additions & 2 deletions Scribe.Sdk/Sdk/Phases/Library.targets
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@
(a companion typically <ProjectReference>s its Library).
-->

<!-- This file is only imported when $(_IsScribeLibrary) is true (gate lives on
the <Import> in Sdk.targets). No per-node conditions needed inside. -->

<!-- ── Library defaults ────────────────────────────────────────────────── -->
<PropertyGroup Condition="'$(_IsScribeLibrary)' == 'true'">
<PropertyGroup>
<!-- Libraries ship their own DLL in lib/. -->
<IncludeBuildOutput Condition="'$(IncludeBuildOutput)' == ''">true</IncludeBuildOutput>
<!-- NU5128 is only relevant when lib/ is empty — Library fills it. -->
Expand All @@ -49,7 +52,7 @@
-->
<Target Name="_ScribeSdkEmbedCompanions"
BeforeTargets="_GetPackageFiles"
Condition="'$(_IsScribeLibrary)' == 'true' and '@(ScribeCompanion)' != ''">
Condition="'@(ScribeCompanion)' != ''">
<MSBuild Projects="@(ScribeCompanion)"
Targets="Build"
Properties="TargetFramework=netstandard2.0;Configuration=$(Configuration);NoBuild=false" />
Expand Down
18 changes: 9 additions & 9 deletions Scribe.Sdk/Sdk/Phases/Meta.targets
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@
</PropertyGroup>
-->

<!-- This file is only imported when $(_IsScribeMeta) is true (gate lives on
the <Import> in Sdk.targets). No per-node conditions needed inside. -->

<!-- ── Meta defaults ───────────────────────────────────────────────────── -->
<PropertyGroup Condition="'$(_IsScribeMeta)' == 'true'">
<PropertyGroup>
<IncludeBuildOutput>false</IncludeBuildOutput>
<PackageType>Analyzer</PackageType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
Expand All @@ -48,16 +51,15 @@
</PropertyGroup>

<!-- ── Artifacts directory (normalized with trailing slash) ────────────── -->
<PropertyGroup Condition="'$(_IsScribeMeta)' == 'true' and '$(ArtifactsPath)' != ''">
<PropertyGroup Condition="'$(ArtifactsPath)' != ''">
<_ScribeMetaArtifactsDir>$([MSBuild]::EnsureTrailingSlash('$(ArtifactsPath)'))</_ScribeMetaArtifactsDir>
<PackageOutputPath>$(_ScribeMetaArtifactsDir)packages\</PackageOutputPath>
<RestoreAdditionalProjectSources>$(RestoreAdditionalProjectSources);$(_ScribeMetaArtifactsDir)packages</RestoreAdditionalProjectSources>
</PropertyGroup>

<!-- ── Own DLL + private deps into analyzers/dotnet/cs/ ────────────────── -->
<Target Name="_ScribeSdkMetaAddAnalyzerDll"
BeforeTargets="_GetPackageFiles"
Condition="'$(_IsScribeMeta)' == 'true'">
BeforeTargets="_GetPackageFiles">
<ItemGroup>
<None Include="$(TargetPath)"
Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
Expand All @@ -66,8 +68,7 @@

<Target Name="_ScribeSdkMetaAddAnalyzerDependencies"
BeforeTargets="_GetPackageFiles"
DependsOnTargets="ResolvePackageAssets"
Condition="'$(_IsScribeMeta)' == 'true'">
DependsOnTargets="ResolvePackageAssets">
<ItemGroup>
<_ScribeSdkMetaDep Include="@(ReferenceCopyLocalPaths)"
Condition="'%(ReferenceCopyLocalPaths.NuGetPackageId)' != ''
Expand All @@ -79,7 +80,7 @@
</Target>

<!-- ── In-solution ProjectReference consumer path ──────────────────────── -->
<Target Name="GetDependencyTargetPaths" Condition="'$(_IsScribeMeta)' == 'true'">
<Target Name="GetDependencyTargetPaths">
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="@(ReferenceCopyLocalPaths)"
IncludeRuntimeDependency="false" />
Expand All @@ -89,8 +90,7 @@
<!-- ── Emit PackageVersion override file after Pack ────────────────────── -->
<Target Name="_ScribeSdkMetaOverride"
AfterTargets="Pack"
Condition="'$(_IsScribeMeta)' == 'true'
and '$(_ScribeMetaArtifactsDir)' != ''
Condition="'$(_ScribeMetaArtifactsDir)' != ''
and '$(PackageId)' != ''">
<PropertyGroup>
<_ScribeMetaOverrideVersion>$(NuGetPackageVersion)</_ScribeMetaOverrideVersion>
Expand Down
4 changes: 3 additions & 1 deletion Scribe.Sdk/Sdk/Phases/Stubs.targets
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
InternalsVisibleTo or a shared linked source.
-->

<ItemGroup Condition="'$(ScribeSdkIncludeStubs)' == 'true'">
<!-- This file is only imported when $(ScribeSdkIncludeStubs) is true (gate
lives on the <Import> in Sdk.targets). No per-node condition needed. -->
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)..\..\content\Stubs.cs"
Link="Generated\Stubs.cs"
Visible="false" />
Expand Down
19 changes: 13 additions & 6 deletions Scribe.Sdk/Sdk/Sdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,21 @@
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />

<!-- Phases. Order matters: Classification sets flags → Validation checks them
→ Stubs injects polyfills → type-specific phases gate on the flags. -->
→ type-specific phases only import when the matching flag is set. Gating
at the Import level (rather than on every PropertyGroup/Target inside)
keeps the per-type files free of per-node conditions that could drift. -->
<Import Project="$(MSBuildThisFileDirectory)Phases\Classification.targets" />
<Import Project="$(MSBuildThisFileDirectory)Phases\Validation.targets" />
<Import Project="$(MSBuildThisFileDirectory)Phases\Stubs.targets" />
<Import Project="$(MSBuildThisFileDirectory)Phases\Library.targets" />
<Import Project="$(MSBuildThisFileDirectory)Phases\Companion.targets" />
<Import Project="$(MSBuildThisFileDirectory)Phases\Analyzer.targets" />
<Import Project="$(MSBuildThisFileDirectory)Phases\Meta.targets" />
<Import Project="$(MSBuildThisFileDirectory)Phases\Stubs.targets"
Condition="'$(ScribeSdkIncludeStubs)' == 'true'" />
<Import Project="$(MSBuildThisFileDirectory)Phases\Library.targets"
Condition="'$(_IsScribeLibrary)' == 'true'" />
<Import Project="$(MSBuildThisFileDirectory)Phases\Companion.targets"
Condition="'$(_IsScribeCompanion)' == 'true'" />
<Import Project="$(MSBuildThisFileDirectory)Phases\Analyzer.targets"
Condition="'$(_IsScribeAnalyzer)' == 'true'" />
<Import Project="$(MSBuildThisFileDirectory)Phases\Meta.targets"
Condition="'$(_IsScribeMeta)' == 'true'" />

<!-- LocalDev producer-side infrastructure (override-file emission, cleanup). -->
<Import Project="$(MSBuildThisFileDirectory)..\build\Scribe.LocalDev.targets" />
Expand Down
3 changes: 3 additions & 0 deletions Scribe.slnx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<Solution>
<Project Path="Scribe/Scribe.csproj" />
<Folder Name="/Solution Items/">
<File Path="version.json" />
</Folder>
<Project Path="Scribe.Ink/Scribe.Ink.csproj" />
<Project Path="Scribe.Scriptorium/Scribe.Scriptorium.csproj" />
<Project Path="Scribe.Sdk/Scribe.Sdk.csproj" />
Expand Down
70 changes: 42 additions & 28 deletions Scribe/build/Scribe.LocalDev.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@

Per-project local NuGet development: each producer declares its own name
via $(ScribesName), and activates "producer mode" when a sentinel file
'.$(ScribesName).user' or '.$(ScribesName).local' exists in $(ScribeRoot).
Consumer-side infrastructure activates whenever *any* such suffix sentinel
is present.
'.$(ScribesName).scribe' exists in $(ScribeRoot). Consumer-side
infrastructure activates whenever *any* such sentinel is present.

── Setup ──────────────────────────────────────────────────────────────

Expand All @@ -23,18 +22,14 @@
── Activation ─────────────────────────────────────────────────────────

Producer mode (auto-pack, timestamped versions, override-file generation):
Create EITHER '.$(ScribesName).user' OR '.$(ScribesName).local' in
$(ScribeRoot), e.g. '.hermetic.user' or '.hermetic.local'. Both forms
are supported: '.user' follows the .NET convention (matches
VisualStudio.gitignore's '*.user' rule out of the box); '.local' reads
more naturally and matches the JS ecosystem (Vite, Next.js). Pick
whichever you prefer — either activates. Lowercase is canonical;
Windows matches case-insensitively, Linux does not.
Create '.$(ScribesName).scribe' in $(ScribeRoot), e.g. '.hermetic.scribe'
or '.scribe.scribe'. Lowercase is canonical (Windows matches case-
insensitively, Linux does not). The '.scribe' suffix is brand-unique
and gitignored via a single '*.scribe' rule.

Consumer mode (local package source, import overrides from artifacts):
Activates when ANY '.*.user' or '.*.local' sentinel exists in
$(ScribeRoot), or when $(IsLocalScribe) is set true via CLI /
Directory.Build.props.
Activates when ANY '.*.scribe' sentinel exists in $(ScribeRoot), or
when $(IsLocalScribe) is set true via CLI / Directory.Build.props.

── Producer Set ────────────────────────────────────────────────────

Expand All @@ -48,10 +43,10 @@

$(IsLocalScribe) 'true' when any local sentinel is active
(umbrella flag — governs consumer-side infra).
$(IsLocalProducer) 'true' when THIS project's '.$(ScribesName).user'
or '.$(ScribesName).local' sentinel exists
(applies to every project in the set — governs
auto-pack and override-file emission).
$(IsLocalProducer) 'true' when THIS project's '.$(ScribesName).scribe'
sentinel exists (applies to every project in
the set — governs auto-pack and override-file
emission).
$(ScribeArtifactsDir) $(ScribeRoot)\.artifacts\
$(ScribePackagesDir) $(ScribeArtifactsDir)packages\

Expand Down Expand Up @@ -84,17 +79,26 @@
<ScribesName>$(ScribeName)</ScribesName>
</PropertyGroup>

<!-- ── Umbrella Sentinel Scan (any .*.user / .*.local) ────────────────── -->
<!-- Any '.*.user' or '.*.local' file at $(ScribeRoot) flips $(IsLocalScribe) on.
This governs consumer-side infrastructure: NuGet source registration
<!-- ── Umbrella Sentinel Scan (.*.scribe) ──────────────────────────────── -->
<!-- Any '.<producer>.scribe' file at $(ScribeRoot) flips $(IsLocalScribe)
on. Governs consumer-side infrastructure: NuGet source registration
and version-override imports.

Examples: '.hermetic.scribe', '.scribe.scribe'.

The '.scribe' suffix is brand-unique — zero risk of collision with
.NET's '.user' (VS project user settings) or the JS ecosystem's
'.local' (Vite/Next.js/prettier env files). Gitignore with a single
line: '*.scribe'.

Forward slashes keep MSBuild's glob engine behaving identically on Linux
and Windows.

NOTE: MSBuild forbids @(ItemList) references inside Condition attributes
(MSB4099). Project the ItemGroup into a property first, then condition
on the property. -->
<ItemGroup Condition="'$(_ScribeLocalDevSkip)' != 'true' and '$(ScribeRoot)' != ''">
<_ScribeLocalSentinel Include="$(ScribeRoot)\.*.user" />
<_ScribeLocalSentinel Include="$(ScribeRoot)\.*.local" />
<_ScribeLocalSentinel Include="$(ScribeRoot)/.*.scribe" />
</ItemGroup>
<PropertyGroup Condition="'$(_ScribeLocalDevSkip)' != 'true'">
<_ScribeLocalSentinelList>@(_ScribeLocalSentinel)</_ScribeLocalSentinelList>
Expand All @@ -107,23 +111,33 @@

<!-- ── Per-Project Producer Sentinel ─────────────────────────────────── -->
<!-- Activates producer mode for the project that declares $(ScribesName).
Matches EITHER '.$(ScribesName).user' OR '.$(ScribesName).local'
(lowercased). Both forms are supported — pick whichever you prefer. -->
Matches '.$(ScribesName).scribe' (lowercased) in $(ScribeRoot). -->
<PropertyGroup Condition="'$(_ScribeLocalDevSkip)' != 'true' and '$(ScribesName)' != '' and '$(ScribeRoot)' != ''">
<_ScribeProducerBase>$(ScribeRoot)\.$(ScribesName.ToLowerInvariant())</_ScribeProducerBase>
<_ScribeProducerSentinel>$(ScribeRoot)\.$(ScribesName.ToLowerInvariant()).scribe</_ScribeProducerSentinel>
</PropertyGroup>
<PropertyGroup Condition="'$(_ScribeLocalDevSkip)' != 'true'
and '$(IsLocalProducer)' != 'true'
and '$(_ScribeProducerBase)' != ''
and (Exists('$(_ScribeProducerBase).user') or Exists('$(_ScribeProducerBase).local'))">
and '$(_ScribeProducerSentinel)' != ''
and Exists('$(_ScribeProducerSentinel)')">
<IsLocalProducer>true</IsLocalProducer>
</PropertyGroup>

<!-- ── Path Setup ──────────────────────────────────────────────────── -->
<PropertyGroup Condition="'$(_ScribeLocalDevSkip)' != 'true' and '$(IsLocalScribe)' == 'true' and '$(ScribeRoot)' != ''">
<ScribeArtifactsDir>$(ScribeRoot)\.artifacts\</ScribeArtifactsDir>
<ScribePackagesDir>$(ScribeArtifactsDir)packages\</ScribePackagesDir>
<!-- Register the local packages directory as a NuGet source so restore finds them. -->
</PropertyGroup>

<!-- Register the local packages directory as a NuGet source so restore finds
them. Only do this if the directory actually exists — NuGet emits NU1301
otherwise ("The local source ... doesn't exist"), which breaks restore on
fresh clones / CI where nothing has been packed yet. The auto-pack block
below is what creates the directory; once it exists, subsequent restores
pick it up. -->
<PropertyGroup Condition="'$(_ScribeLocalDevSkip)' != 'true'
and '$(IsLocalScribe)' == 'true'
and '$(ScribePackagesDir)' != ''
and Exists('$(ScribePackagesDir)')">
<RestoreAdditionalProjectSources>
$(RestoreAdditionalProjectSources);$(ScribePackagesDir)
</RestoreAdditionalProjectSources>
Expand Down
6 changes: 3 additions & 3 deletions Scribe/build/Scribe.LocalDev.targets
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
Scribe.LocalDev.props:

$(ScribesName) Producer project name. Used for:
• sentinel detection: '.$(ScribesName).user' or '.$(ScribesName).local'
• sentinel detection: '.$(ScribesName).scribe'
• auto-pack trigger: $(MSBuildProjectName) == $(ScribesName)
• override file name: $(ScribesName).Directory.Packages.targets
Comment on lines 25 to 28

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

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

The comments here say the override file name is based on $(ScribesName), but the implementation below writes/deletes override files using $(PackageId) (see _ScribeDevOverridePath / _ScribeCleanupOverride). Update this documentation so users aren’t misled about the file naming contract.

Copilot uses AI. Check for mistakes.
Example: Hermetic
Expand Down Expand Up @@ -110,8 +110,8 @@

<!-- ── Cleanup When Sentinel Is Removed ───────────────────────────────── -->
<!--
When a project in the producer set builds WITHOUT its '.$(ScribesName).user'
or '.$(ScribesName).local' sentinel, delete its own stale override file
When a project in the producer set builds WITHOUT its '.$(ScribesName).scribe'
sentinel, delete its own stale override file
and stale -dev.* packages. Runs per-project (each project cleans up its
own PackageId-keyed artifacts) so a newly-added project's artifacts get
cleaned once it too builds sentinel-less.
Expand Down
Loading
Loading