From 3d8d1339d6b67d278386eff51a9f9401a2f965d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Sim=C3=B5es?= Date: Wed, 24 Jun 2026 18:11:39 +0100 Subject: [PATCH 1/2] Move build tasks into SDK and add cross-platform resource parser - Copy 6 build-task sources from `Tools.BuildTasks-2019` into `src\nanoFramework.Tools.BuildTasks\`. - Remove cross-repo `` links and comment block from `nanoFramework.Tools.BuildTasks.csproj`. - Delete `GenerateNanoResourceTask.Stub.cs`. - Add `ProcessResourceFiles.CrossPlatform.cs`: replacing `System.Drawing`, `AppDomain`, `BinaryFormatter`, and WinForms `ResXResourceReader` with `SixLabors.ImageSharp`, `System.Resources.Extensions`, `System.Text.Json`, and direct XML parsing. - Add `PackageReference`s for `SixLabors.ImageSharp`, `System.Resources.Extensions`, `System.CodeDom` . - Pack new runtime deps under `tasks\net8.0\` in `nanoFramework.NET.Sdk.csproj`. - Update `nanoFramework.Resources.targets` wiring. - Add `BuildTasks.Tests` with unit tests for `NanoResxResourceReader`, `ProcessResourceFiles`, and RGB565 conversion. - Add `SmokeTest` resources (`.resx`, bitmap, binary blob) for end-to-end validation. Assisted by Claude Opus 4.8 --- azure-pipelines.yml | 39 + nanoFramework.NET.Sdk.sln | 6 + .../Sdk/nanoFramework.Resources.targets | 54 +- .../nanoFramework.NET.Sdk.csproj | 4 + .../GenerateBinaryOutputTask.cs | 86 + .../GenerateNanoResourceTask.Stub.cs | 53 - .../GenerateNanoResourceTask.cs | 1278 ++++++++++ .../ProcessResourceFiles.CrossPlatform.cs | 373 +++ .../ProcessResourceFiles.cs | 2168 +++++++++++++++++ .../ResolveRuntimeDependenciesTask.cs | 66 + .../TasksConstants.cs | 13 + .../Utilities/DebuggerHelper.cs | 47 + .../nanoFramework.Tools.BuildTasks.csproj | 44 +- test/BuildTasks.Tests/BuildTasks.Tests.csproj | 38 + .../Infrastructure/TestBuildEngine.cs | 49 + .../NanoResxResourceReaderTests.cs | 255 ++ .../ProcessResourceFilesTests.cs | 320 +++ .../BuildTasks.Tests/Rgb565ConversionTests.cs | 228 ++ test/SmokeTest/Resources.resx | 48 + test/SmokeTest/SmokeTest.csproj | 2 + test/SmokeTest/data.bin | 1 + test/SmokeTest/logo.bmp | Bin 0 -> 118 bytes 22 files changed, 5094 insertions(+), 78 deletions(-) create mode 100644 nanoFramework.Tools.BuildTasks/GenerateBinaryOutputTask.cs delete mode 100644 nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.Stub.cs create mode 100644 nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.cs create mode 100644 nanoFramework.Tools.BuildTasks/ProcessResourceFiles.CrossPlatform.cs create mode 100644 nanoFramework.Tools.BuildTasks/ProcessResourceFiles.cs create mode 100644 nanoFramework.Tools.BuildTasks/ResolveRuntimeDependenciesTask.cs create mode 100644 nanoFramework.Tools.BuildTasks/TasksConstants.cs create mode 100644 nanoFramework.Tools.BuildTasks/Utilities/DebuggerHelper.cs create mode 100644 test/BuildTasks.Tests/BuildTasks.Tests.csproj create mode 100644 test/BuildTasks.Tests/Infrastructure/TestBuildEngine.cs create mode 100644 test/BuildTasks.Tests/NanoResxResourceReaderTests.cs create mode 100644 test/BuildTasks.Tests/ProcessResourceFilesTests.cs create mode 100644 test/BuildTasks.Tests/Rgb565ConversionTests.cs create mode 100644 test/SmokeTest/Resources.resx create mode 100644 test/SmokeTest/data.bin create mode 100644 test/SmokeTest/logo.bmp diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f0b55d4..854b022 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -119,6 +119,29 @@ stages: # displayName: 'Publish Quality Gate Result' # continueOnError: true + - task: VisualStudioTestPlatformInstaller@1 + condition: succeeded() + displayName: 'Install VS Test Platform' + inputs: + versionSelector: latestStable + + - task: VSTest@2 + condition: succeeded() + displayName: 'Running Unit Tests' + continueOnError: false + inputs: + testSelector: 'testAssemblies' + testAssemblyVer2: | + **\*Tests*.dll + !**\obj\** + !**\TestAdapter\** + searchFolder: '$(System.DefaultWorkingDirectory)' + platform: '$(BuildPlatform)' + configuration: '$(BuildConfiguration)' + diagnosticsEnabled: true + vsTestVersion: toolsInstaller + codeCoverageEnabled: true + - task: DotNetCoreCLI@2 displayName: Install Sign Client CLI condition: >- @@ -220,6 +243,22 @@ stages: command: 'build' projects: 'test/SmokeTest/SmokeTest.csproj' arguments: '--configuration $(buildConfiguration) --no-restore' + + - task: PowerShell@2 + displayName: Validate SmokeTest native checksum + inputs: + targetType: inline + script: | + $expectedChecksum = '0x1DA81E42' + $actualChecksum = $env:NF_NATIVE_ASSEMBLY_CHECKSUM + + if ($actualChecksum -ine $expectedChecksum) + { + Write-Host "##vso[task.logissue type=error]Smoke test assembly checksum mismatch: expected $expectedChecksum but got '$actualChecksum'." + exit 1 + } + + Write-Host "Smoke test assembly checksum matches expected value: $expectedChecksum." - task: NuGetCommand@2 condition: >- diff --git a/nanoFramework.NET.Sdk.sln b/nanoFramework.NET.Sdk.sln index 2c0f67e..c52ad53 100644 --- a/nanoFramework.NET.Sdk.sln +++ b/nanoFramework.NET.Sdk.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "nanoFramework.Tools.BuildTa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "nanoFramework.NET.Sdk", "nanoFramework.NET.Sdk\nanoFramework.NET.Sdk.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildTasks.Tests", "test\BuildTasks.Tests\BuildTasks.Tests.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,5 +23,9 @@ Global {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/nanoFramework.NET.Sdk/Sdk/nanoFramework.Resources.targets b/nanoFramework.NET.Sdk/Sdk/nanoFramework.Resources.targets index 9992936..a1cea5a 100644 --- a/nanoFramework.NET.Sdk/Sdk/nanoFramework.Resources.targets +++ b/nanoFramework.NET.Sdk/Sdk/nanoFramework.Resources.targets @@ -28,15 +28,61 @@ Condition="'$(NF_MSBUILDTASK_PATH)' != ''" AssemblyFile="$(NF_MSBUILDTASK_PATH)\nanoFramework.Tools.BuildTasks.dll" /> + + + + <_NanoResxItems Include="@(EmbeddedResource)" + Condition="'%(EmbeddedResource.Type)' == 'Resx'" /> + + + + + + + + <_NanoResManagerRef Include="@(PackageReference)" + Condition="'%(Identity)' == 'nanoFramework.ResourceManager'" /> + + + + + Condition="'@(_NanoResxItems)' != ''" + DependsOnTargets="_NanoInterceptResxItems;_NanoCheckResourceManagerRef"> + OutputResources="@(_NanoResxItems->'$(IntermediateOutputPath)%(ManifestResourceName).nanoresources')"> diff --git a/nanoFramework.NET.Sdk/nanoFramework.NET.Sdk.csproj b/nanoFramework.NET.Sdk/nanoFramework.NET.Sdk.csproj index 8b66c0d..f5a5591 100644 --- a/nanoFramework.NET.Sdk/nanoFramework.NET.Sdk.csproj +++ b/nanoFramework.NET.Sdk/nanoFramework.NET.Sdk.csproj @@ -102,6 +102,10 @@ + + + + diff --git a/nanoFramework.Tools.BuildTasks/GenerateBinaryOutputTask.cs b/nanoFramework.Tools.BuildTasks/GenerateBinaryOutputTask.cs new file mode 100644 index 0000000..1bca269 --- /dev/null +++ b/nanoFramework.Tools.BuildTasks/GenerateBinaryOutputTask.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using nanoFramework.Tools.Utilities; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; + +namespace nanoFramework.Tools +{ + [Description("GenerateBinaryOutputTaskEntry")] + + public class GenerateBinaryOutputTask : Task + { + #region public properties for the task + + public string Assembly { get; set; } + + public string AssemblyPE { get; set; } + + public ITaskItem[] AssemblyReferences { get; set; } + + + /// + /// The name(s) of binary file created. + /// + [Output] + public ITaskItem FileWritten { get; private set; } + + #endregion + + public override bool Execute() + { + // report to VS output window what step the build is + Log.LogMessage(MessageImportance.Normal, "Generating binary output file..."); + + // wait for debugger on var + DebuggerHelper.WaitForDebuggerIfEnabled(TasksConstants.BuildTaskDebugVar); + + // default with null, indicating that we've generated nothing + FileWritten = null; + + // get paths for PE files + // rename extension .dll with .pe + List peCollection = new List(); + peCollection = AssemblyReferences?.Select(a => { return a.GetMetadata("FullPath").Replace(".dll", ".pe").Replace(".exe", ".pe"); }).ToList(); + + // add executable PE + peCollection.Add(AssemblyPE); + + // get executable path and file name + // rename executable extension .exe with .bin + var binOutputFile = Assembly.Replace(".exe", ".bin"); + + using (FileStream binFile = new FileStream(binOutputFile, FileMode.Create)) + { + // now we will re-deploy all system assemblies + foreach (string peItem in peCollection) + { + // append to the deploy blob the assembly + using (FileStream fs = File.Open(peItem, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + long length = (fs.Length + 3) / 4 * 4; + byte[] buffer = new byte[length]; + + fs.Read(buffer, 0, (int)fs.Length); + + // copy this assembly to the bin file too + binFile.Write(buffer, 0, (int)length); + } + } + } + + // bin file written + FileWritten = new TaskItem(binOutputFile); + + return true; + } + } +} diff --git a/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.Stub.cs b/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.Stub.cs deleted file mode 100644 index 1187d0e..0000000 --- a/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.Stub.cs +++ /dev/null @@ -1,53 +0,0 @@ -// -// Copyright (c) .NET Foundation and Contributors -// See LICENSE file in the project root for full license information. -// -// Stub implementation of GenerateNanoResourceTask for .NET 8+. -// The full implementation uses System.Windows.Forms ResXResourceReader and -// AppDomain APIs not available on modern .NET. Resource compilation is -// supported when building with full .NET Framework (msbuild.exe / VS). -// - -#if !NETFRAMEWORK - -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using System.ComponentModel; - -namespace nanoFramework.Tools -{ - [Description("GenerateNanoResourceTaskEntry")] - public class GenerateNanoResourceTask : Task - { - public ITaskItem[] Sources { get; set; } - public ITaskItem[] References { get; set; } - public bool UseSourcePath { get; set; } - public string StateFile { get; set; } - - [Output] - public ITaskItem[] OutputResources { get; set; } - - [Output] - public ITaskItem[] FilesWritten { get; set; } - - public override bool Execute() - { - if (Sources == null || Sources.Length == 0) - { - return true; - } - - Log.LogWarning( - "NFSDK0001", - "NFSDK0001", - null, null, 0, 0, 0, 0, - "GenerateNanoResourceTask is not yet supported when building with 'dotnet build'. " + - "Resource (.resx) compilation requires building with full .NET Framework (msbuild.exe or Visual Studio). " + - "This limitation will be addressed in a future SDK version."); - - return true; - } - } -} - -#endif diff --git a/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.cs b/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.cs new file mode 100644 index 0000000..e0dca48 --- /dev/null +++ b/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.cs @@ -0,0 +1,1278 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using nanoFramework.Tools.Utilities; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Resources; +using System.Security; +using System.Text; +using System.Xml; + +namespace nanoFramework.Tools +{ + static internal class ExceptionHandling + { + /// + /// If the given exception is file IO related then return. + /// Otherwise, rethrow the exception. + /// + /// The exception to check. + internal static void RethrowUnlessFileIO(Exception e) + { + if + ( + e is UnauthorizedAccessException + || e is ArgumentNullException + || e is PathTooLongException + || e is DirectoryNotFoundException + || e is NotSupportedException + || e is ArgumentException + || e is SecurityException + || e is IOException + ) + { + return; + } + + Debug.Assert(false, "Exception unexpected for this File IO case. Please open a bug that we need to add a 'catch' block for this exception. Look at the build log for more details including a stack trace."); + throw e; + } + } + + + /// + /// Represents a cache of inputs to a compilation-style task. + /// + [Serializable()] + internal class Dependencies + { + /// + /// Hashtable of other dependency files. + /// Key is filename and value is DependencyFile. + /// + private Hashtable dependencies = new Hashtable(); + + /// + /// Look up a dependency file. Return null if its not there. + /// + /// + /// + internal DependencyFile GetDependencyFile(string filename) + { + return (DependencyFile)dependencies[filename]; + } + + + /// + /// Add a new dependency file. + /// + /// + /// + internal void AddDependencyFile(string filename, DependencyFile file) + { + dependencies[filename] = file; + } + + /// + /// Remove new dependency file. + /// + /// + /// + internal void RemoveDependencyFile(string filename) + { + dependencies.Remove(filename); + } + + /// + /// Remove all entries from the dependency table. + /// + internal void Clear() + { + dependencies.Clear(); + } + + /// + /// All dependency files currently cached. + /// + internal ICollection Values + { + get { return dependencies.Values; } + } + } + + + /// + /// Represents a single input to a compilation-style task. + /// Keeps track of timestamp for later comparison. + /// + [Serializable] + internal class DependencyFile + { + + // Whether the file exists or not. + readonly bool exists = false; + + /// + /// The name of the file. + /// + /// + internal string FileName { get; } + + /// + /// The last-modified timestamp when the class was instantiated. + /// + /// + internal DateTime LastModified { get; } + + /// + /// Returns true if the file existed when this class was instantiated. + /// + /// + internal bool Exists + { + get { return exists; } + } + + /// + /// Construct. + /// + /// The file name. + internal DependencyFile(string filename) + { + FileName = filename; + + if (File.Exists(FileName)) + { + LastModified = File.GetLastWriteTime(FileName); + exists = true; + } + else + { + exists = false; + } + } + + /// + /// Construct from previously cached values (used when deserializing the state file). + /// + internal DependencyFile(string filename, DateTime lastModified, bool exists) + { + FileName = filename; + LastModified = lastModified; + this.exists = exists; + } + + /// + /// Checks whether the file has changed since the last time a timestamp was recorded. + /// + /// + internal bool HasFileChanged() + { + // Obviously if the file no longer exists then we are not up to date. + if (!File.Exists(FileName)) + { + return true; + } + + // Check the saved timestamp against the current timestamp. + // If they are different then obviously we are out of date. + DateTime curLastModified = File.GetLastWriteTime(FileName); + if (curLastModified != LastModified) + { + return true; + } + + // All checks passed -- the info should still be up to date. + return false; + } + } + + /// + /// Base class for task state files. + /// + /// + /// Base class for task state files. + /// The state file is an incremental-build optimization (it caches the linked + /// files referenced by each .resx and their timestamps). It is serialized with + /// a simple, self-describing binary format so it works identically on .NET + /// Framework and modern .NET (the previous BinaryFormatter-based format is not + /// available on net8.0+). A corrupt or out-of-date cache is never fatal: callers + /// fall back to a fresh cache, so the format/version can change freely. + /// + internal abstract class StateFileBase + { + /// + /// Magic + version guarding the cache format. Bump + /// to invalidate caches written by an older build task. + /// + private const uint CacheMagic = 0x4E464443; // "NFDC" + protected const int CacheVersion = 1; + + /// + /// Default constructor + /// + internal StateFileBase() + { + // do nothing + } + + /// + /// Writes the derived state content to the (already opened) cache stream. + /// + protected abstract void WriteContent(BinaryWriter writer); + + /// + /// Writes the contents of this object out to the specified file. + /// + /// + virtual internal void SerializeCache(string stateFile, TaskLoggingHelper log) + { + try + { + if (stateFile != null && stateFile.Length > 0) + { + if (File.Exists(stateFile)) + { + File.Delete(stateFile); + } + + using (FileStream s = new FileStream(stateFile, FileMode.CreateNew)) + using (BinaryWriter writer = new BinaryWriter(s, Encoding.UTF8)) + { + writer.Write(CacheMagic); + writer.Write(CacheVersion); + WriteContent(writer); + } + } + } + catch (Exception e) + { + // If there was a problem writing the file (like it's read-only or locked on disk, for + // example), then eat the exception and log a warning. Otherwise, rethrow. + ExceptionHandling.RethrowUnlessFileIO(e); + + // Not being able to serialize the cache is not an error, but we let the user know anyway. + // Don't want to hold up processing just because we couldn't read the file. + log.LogWarning("Could not write state file {0} ({1})", stateFile, e.Message); + } + } + + /// + /// Opens the state file and validates the header. Returns a positioned + /// ready for the derived content, or null when the + /// file is missing, unreadable, or written by an incompatible version. + /// + protected static BinaryReader OpenCacheForRead(string stateFile, TaskLoggingHelper log) + { + try + { + if (stateFile != null && stateFile.Length > 0 && File.Exists(stateFile)) + { + FileStream s = new FileStream(stateFile, FileMode.Open); + BinaryReader reader = new BinaryReader(s, Encoding.UTF8); + + if (reader.ReadUInt32() == CacheMagic && reader.ReadInt32() == CacheVersion) + { + return reader; + } + + reader.Dispose(); + } + } + catch (Exception e) + { + // Not being able to deserialize the cache is not an error, but we let the user know anyway. + log.LogWarning("Could not read state file {0} ({1})", stateFile, e.Message); + } + + return null; + } + + /// + /// Deletes the state file from disk + /// + /// + /// + static internal void DeleteFile(string stateFile, TaskLoggingHelper log) + { + try + { + if (stateFile != null && + stateFile.Length > 0 && + File.Exists(stateFile)) + { + File.Delete(stateFile); + } + } + catch (Exception e) + { + // If there was a problem deleting the file (like it's read-only or locked on disk, for + // example), then eat the exception and log a warning. Otherwise, rethrow. + ExceptionHandling.RethrowUnlessFileIO(e); + + log.LogWarning("Could not delete state file {0} ({1})", stateFile, e.Message); + } + } + } + + + /// + /// This class is a caching mechanism for the resgen task to keep track of linked + /// files within processed .resx files. + /// + [Serializable()] + internal sealed class ResGenDependencies : StateFileBase + { + /// + /// The list of resx files. + /// + private Dependencies resXFiles = new Dependencies(); + + /// + /// A newly-created ResGenDependencies is not dirty. + /// What would be the point in saving the default? + /// + [NonSerialized] + private bool isDirty = false; + + /// + /// This is the directory that will be used for resolution of files linked within a .resx. + /// If this is NULL then we use the directory in which the .resx is in (that should always + /// be the default!) + /// + private string baseLinkedFileDirectory; + + /// + /// Construct. + /// + internal ResGenDependencies() + { + } + + internal string BaseLinkedFileDirectory + { + get + { + return baseLinkedFileDirectory; + } + set + { + if (value == null && baseLinkedFileDirectory == null) + { + // No change + } + else if ((value == null && baseLinkedFileDirectory != null) || + (value != null && baseLinkedFileDirectory == null) || + (String.Compare(baseLinkedFileDirectory, value, true, CultureInfo.InvariantCulture) != 0)) + { + // Ok, this is slightly complicated. Changing the base directory in any manner may + // result in changes to how we find .resx files. Therefore, we must clear our out + // cache whenever the base directory changes. + resXFiles.Clear(); + isDirty = true; + baseLinkedFileDirectory = value; + } + } + } + + internal bool UseSourcePath + { + set + { + // Ensure that the cache is properly initialized with respect to how resgen will + // resolve linked files within .resx files. ResGen has two different + // ways for resolving relative file-paths in linked files. The way + // that ResGen resolved relative paths before Whidbey was always to + // resolve from the current working directory. In Whidbey a new command-line + // switch "/useSourcePath" instructs ResGen to use the folder that + // contains the .resx file as the path from which it should resolve + // relative paths. So we should base our timestamp/existence checking + // on the same switch & resolve in the same manner as ResGen. + BaseLinkedFileDirectory = value ? null : Environment.CurrentDirectory; + } + } + + internal ResXFile GetResXFileInfo(string resxFile) + { + // First, try to retrieve the resx information from our hashtable. + ResXFile retVal = (ResXFile)resXFiles.GetDependencyFile(resxFile); + + if (retVal == null) + { + // Ok, the file wasn't there. Add it to our cache and return it to the caller. + retVal = AddResxFile(resxFile); + } + else + { + // The file was there. Is it up to date? If not, then we'll have to refresh the file + // by removing it from the hashtable and readding it. + if (retVal.HasFileChanged()) + { + resXFiles.RemoveDependencyFile(resxFile); + isDirty = true; + retVal = AddResxFile(resxFile); + } + } + + return retVal; + } + + private ResXFile AddResxFile(string file) + { + // This method adds a .resx file "file" to our .resx cache. The method causes the file + // to be cracked for contained files. + + ResXFile resxFile = new ResXFile(file, BaseLinkedFileDirectory); + resXFiles.AddDependencyFile(file, resxFile); + isDirty = true; + return resxFile; + } + /// + /// Writes the contents of this object out to the specified file. + /// + /// + override internal void SerializeCache(string stateFile, TaskLoggingHelper log) + { + base.SerializeCache(stateFile, log); + isDirty = false; + } + + /// + /// Writes the cached .resx dependency information (see ). + /// + protected override void WriteContent(BinaryWriter writer) + { + writer.Write(baseLinkedFileDirectory != null); + if (baseLinkedFileDirectory != null) + { + writer.Write(baseLinkedFileDirectory); + } + + ICollection values = resXFiles.Values; + writer.Write(values.Count); + + foreach (ResXFile resxFile in values) + { + writer.Write(resxFile.FileName); + writer.Write(resxFile.LastModified.ToBinary()); + writer.Write(resxFile.Exists); + + if (resxFile.LinkedFiles == null) + { + writer.Write(-1); + } + else + { + writer.Write(resxFile.LinkedFiles.Length); + foreach (string linked in resxFile.LinkedFiles) + { + writer.Write(linked ?? string.Empty); + } + } + } + } + + /// + /// Reads the cached .resx dependency information back from the state file. + /// + private void ReadContent(BinaryReader reader) + { + baseLinkedFileDirectory = reader.ReadBoolean() ? reader.ReadString() : null; + + int count = reader.ReadInt32(); + for (int i = 0; i < count; i++) + { + string fileName = reader.ReadString(); + DateTime lastModified = DateTime.FromBinary(reader.ReadInt64()); + bool exists = reader.ReadBoolean(); + + int linkedCount = reader.ReadInt32(); + string[] linkedFiles = null; + if (linkedCount >= 0) + { + linkedFiles = new string[linkedCount]; + for (int j = 0; j < linkedCount; j++) + { + linkedFiles[j] = reader.ReadString(); + } + } + + resXFiles.AddDependencyFile(fileName, new ResXFile(fileName, lastModified, exists, linkedFiles)); + } + } + + /// + /// Reads the .cache file from disk into a ResGenDependencies object. + /// + /// + /// + /// + internal static ResGenDependencies DeserializeCache(string stateFile, bool useSourcePath, TaskLoggingHelper log) + { + ResGenDependencies retVal = new ResGenDependencies(); + + try + { + using (BinaryReader reader = OpenCacheForRead(stateFile, log)) + { + if (reader != null) + { + retVal.ReadContent(reader); + } + } + } + catch (Exception e) + { + // A corrupt or partially-written cache is not fatal: start fresh. + log.LogWarning("Could not read state file {0} ({1})", stateFile, e.Message); + retVal = new ResGenDependencies(); + } + + // Ensure that the cache is properly initialized with respect to how resgen will + // resolve linked files within .resx files. ResGen has two different + // ways for resolving relative file-paths in linked files. The way + // that ResGen resolved relative paths before Whidbey was always to + // resolve from the current working directory. In Whidbey a new command-line + // switch "/useSourcePath" instructs ResGen to use the folder that + // contains the .resx file as the path from which it should resolve + // relative paths. So we should base our timestamp/existence checking + // on the same switch & resolve in the same manner as ResGen. + retVal.UseSourcePath = useSourcePath; + + return retVal; + } + + /// + /// Represents a single .resx file in the dependency cache. + /// + [Serializable()] + internal sealed class ResXFile : DependencyFile + { + internal string[] LinkedFiles { get; } + + internal ResXFile(string filename, string baseLinkedFileDirectory) + : base(filename) + { + // Creates a new ResXFile object and populates the class member variables + // by computing a list of linked files within the .resx that was passed in. + // + // filename is the filename of the .resx file that is to be examined. + + if (File.Exists(FileName)) + { + LinkedFiles = ResXFile.GetLinkedFiles(filename, baseLinkedFileDirectory); + } + } + + /// + /// Construct from previously cached values (used when deserializing the state file). + /// + internal ResXFile(string filename, DateTime lastModified, bool exists, string[] linkedFiles) + : base(filename, lastModified, exists) + { + LinkedFiles = linkedFiles; + } + + /// + /// Given a .RESX file, returns all the linked files that are referenced within that .RESX. + /// + /// + /// + /// + /// May be thrown if Resx is invalid. May contain XmlException. + /// May be thrown if Resx is invalid + internal static string[] GetLinkedFiles(string filename, string baseLinkedFileDirectory) + { + // This method finds all linked .resx files for the .resx file that is passed in. + // filename is the filename of the .resx file that is to be examined. + + // Construct the return array + ArrayList retVal = new ArrayList(); + +#if NETFRAMEWORK + using (ResXResourceReader resxReader = new ResXResourceReader(filename)) + { + // Tell the reader to return ResXDataNode's instead of the object type + // the resource becomes at runtime so we can figure out which files + // the .resx references + resxReader.UseResXDataNodes = true; + + // First we need to figure out where the linked file resides in order + // to see if it exists & compare its timestamp, and we need to do that + // comparison in the same way ResGen does it. ResGen has two different + // ways for resolving relative file-paths in linked files. The way + // that ResGen resolved relative paths before Whidbey was always to + // resolve from the current working directory. In Whidbey a new command-line + // switch "/useSourcePath" instructs ResGen to use the folder that + // contains the .resx file as the path from which it should resolve + // relative paths. So we should base our timestamp/existence checking + // on the same switch & resolve in the same manner as ResGen. + resxReader.BasePath = baseLinkedFileDirectory ?? Path.GetDirectoryName(filename); + + foreach (DictionaryEntry dictEntry in resxReader) + { + if (dictEntry.Value is ResXDataNode) + { + ResXFileRef resxFileRef = ((ResXDataNode)dictEntry.Value).FileRef; + if (resxFileRef != null) + retVal.Add(resxFileRef.FileName); + } + } + } +#else + // Cross-platform: parse the .resx XML directly and collect the file path + // (first token) of every ResXFileRef. Paths are returned verbatim and + // resolved later against the base directory by the caller, matching the + // behaviour of ResXFileRef.FileName on .NET Framework. + XmlDocument doc = new XmlDocument(); + doc.Load(filename); + + XmlNodeList dataNodes = doc.SelectNodes("/root/data"); + if (dataNodes != null) + { + foreach (XmlNode dataNode in dataNodes) + { + string type = dataNode.Attributes?["type"]?.Value; + if (string.IsNullOrEmpty(type) || !type.Contains("ResXFileRef")) + { + continue; + } + + XmlNode valueNode = dataNode.SelectSingleNode("value"); + string raw = valueNode != null ? valueNode.InnerText : null; + if (string.IsNullOrEmpty(raw)) + { + continue; + } + + // value is "path;type[;encoding]" — a quoted path may itself contain ';'. + string path = raw.Trim(); + if (path.StartsWith("\"")) + { + int closingQuote = path.IndexOf('"', 1); + if (closingQuote > 0) + { + path = path.Substring(1, closingQuote - 1); + } + } + else + { + int separator = path.IndexOf(';'); + if (separator >= 0) + { + path = path.Substring(0, separator); + } + } + + retVal.Add(path); + } + } +#endif + return (string[])retVal.ToArray(typeof(string)); + } + } + + /// + /// Whether this cache is dirty or not. + /// + internal bool IsDirty + { + get + { + return isDirty; + } + } + } + + [Description("GenerateNanoResourceTaskEntry")] + public class GenerateNanoResourceTask : Task + { + /// + /// List of output files that we failed to create due to an error. + /// See note in RemoveUnsuccessfullyCreatedResourcesFromOutputResources() + /// + private readonly List _unsuccessfullyCreatedOutFiles = new List(); + + // This cache helps us track the linked resource files listed inside of a resx resource file + private ResGenDependencies cache; + + #region public properties for the task + + /// + /// The names of the items to be converted. The extension must be one of the + // following: .txt, .resx or .resources. + /// + [Required] + public ITaskItem[] Sources { get; set; } + + /// + /// Indicates whether the resource reader should use the source file's directory to + /// resolve relative file paths. + /// + public bool UseSourcePath { get; set; } + + /// + /// Resolves types in ResX files (XML resources) for Strongly Typed Resources + /// + public ITaskItem[] References { get; set; } + + /// + /// This is the path/name of the file containing the dependency cache + /// + public ITaskItem StateFile { get; set; } + + /// + /// The name(s) of the resource file to create. If the user does not specify this + /// attribute, the task will append a .resources extension to each input filename + /// argument and write the file to the directory that contains the input file. + /// Includes any output files that were already up to date, but not any output files + /// that failed to be written due to an error. + /// + [Output] + public ITaskItem[] OutputResources { get; set; } + + /// + /// Storage for names of *all files* written to disk. This is part of the implementation + /// for Clean, and contains the OutputResources items and the StateFile item. + /// Includes any output files that were already up to date, but not any output files + /// that failed to be written due to an error. + /// + [Output] + public ITaskItem[] FilesWritten { get { return _filesWritten.ToArray(); } } + private readonly List _filesWritten = new List(); + + /// + /// (default = false) + /// When true, a new AppDomain is always created to evaluate the .resx files. + /// When false, a new AppDomain is created only when it looks like a user's + /// assembly is referenced by the .resx. + /// + public bool NeverLockTypeAssemblies { get; set; } + + + #endregion + + public override bool Execute() + { + // report to VS output window what step the build is + Log.LogMessage(MessageImportance.Normal, "Generating nanoResources nanoFramework assembly..."); + + // wait for debugger on var + DebuggerHelper.WaitForDebuggerIfEnabled(TasksConstants.BuildTaskDebugVar); + + try + { + // If there are no sources to process, just return (with success) and report the condition. + if ((Sources == null) || (Sources.Length == 0)) + { + Log.LogMessage(MessageImportance.Low, "GenerateResource.NoSources"); + + // Indicate we generated nothing + OutputResources = null; + + return true; + } + + if (!ValidateParameters()) + { + // Indicate we generated nothing + OutputResources = null; + return false; + } + + // In the case that OutputResources wasn't set, build up the outputs by transforming the Sources + if (!CreateOutputResourcesNames()) + { + // Indicate we generated nothing + OutputResources = null; + return false; + } + + // First we look to see if we have a resgen linked files cache. If so, then we can use that + // cache to speed up processing. + ReadStateFile(); + + bool nothingOutOfDate = true; + + List inputsToProcess = new List(); + List outputsToProcess = new List(); + + // decide what sources we need to build + for (int i = 0; i < Sources.Length; ++i) + { + // Attributes from input items are forwarded to output items. + + if (!File.Exists(Sources[i].ItemSpec)) + { + // Error but continue with the files that do exist + Log.LogError("GenerateResource.ResourceNotFound", Sources[i].ItemSpec); + _unsuccessfullyCreatedOutFiles.Add(OutputResources[i].ItemSpec); + } + else + { + // check to see if the output resources file (and, if it is a .resx, any linked files) + // is up to date compared to the input file + if (ShouldRebuildResgenOutputFile(Sources[i].ItemSpec, OutputResources[i].ItemSpec)) + { + nothingOutOfDate = false; + + inputsToProcess.Add(Sources[i]); + outputsToProcess.Add(OutputResources[i]); + } + } + } + + if (nothingOutOfDate) + { + Log.LogMessage("GenerateResource.NothingOutOfDate"); + } + else + { + // Prepare list of referenced assemblies + AssemblyName[] assemblyList; + try + { //only load system.drawing, mscorlib. no parameters needed here?!! + assemblyList = LoadReferences(); + } + catch (ArgumentException e) + { + Log.LogError("GenerateResource.ReferencedAssemblyNotFound - {0}: {1}", e.ParamName, e.Message); + OutputResources = null; + return false; + } + + ProcessResourceFiles process = null; +#if NETFRAMEWORK + // always create a separate AppDomain because an assembly would be locked. + AppDomain appDomain = null; +#endif + + try + { +#if NETFRAMEWORK + appDomain = AppDomain.CreateDomain + ( + "generateResourceAppDomain", + null, + AppDomain.CurrentDomain.SetupInformation + ); + + object obj = appDomain.CreateInstanceFromAndUnwrap + ( + typeof(ProcessResourceFiles).Module.FullyQualifiedName, + typeof(ProcessResourceFiles).FullName + ); + + process = (ProcessResourceFiles)obj; +#else + // On modern .NET there is a single AppDomain, so run in-process. + // The assembly-locking that motivates the separate AppDomain on + // .NET Framework does not apply to the cross-platform .resx reader. + process = new ProcessResourceFiles(); +#endif + + //setup strongly typed class name?? + + process.Run(Log, assemblyList, inputsToProcess.ToArray(), outputsToProcess.ToArray(), + UseSourcePath); + + if (null != process.UnsuccessfullyCreatedOutFiles) + { + foreach (string item in process.UnsuccessfullyCreatedOutFiles) + { + _unsuccessfullyCreatedOutFiles.Add(item); + } + } + } + finally + { +#if NETFRAMEWORK + if (appDomain != null) + { + AppDomain.Unload(appDomain); + } +#endif + } + } + + // And now we serialize the cache to save our resgen linked file resolution for later use. + WriteStateFile(); + + RemoveUnsuccessfullyCreatedResourcesFromOutputResources(); + + RecordFilesWritten(); + } + catch (Exception ex) + { + Log.LogError(".NET nanoFramework GenerateNanoResourceTask error: " + ex.Message); + } + + // if we've logged any errors that's because there were errors (WOW!) + return !Log.HasLoggedErrors; + } + + /// + /// Check for parameter errors. + /// + /// true if parameters are valid + private bool ValidateParameters() + { + // make sure that if the output resources were set, they exactly match the number of input sources + if ((OutputResources != null) && (OutputResources.Length != Sources.Length)) + { + Log.LogError("General.TwoVectorsMustHaveSameLength", Sources.Length, OutputResources.Length, "Sources", "OutputResources"); + return false; + } + + return true; + } + + /// + /// Make sure that OutputResources has 1 file name for each name in Sources. + /// + private bool CreateOutputResourcesNames() + { + if (OutputResources == null) + { + OutputResources = new ITaskItem[Sources.Length]; + + int i = 0; + try + { + for (i = 0; i < Sources.Length; ++i) + { + OutputResources[i] = new TaskItem(Path.ChangeExtension(Sources[i].ItemSpec, ".nanoresources")); + } + } + catch (ArgumentException e) + { + Log.LogError("GenerateResource.InvalidFilename", Sources[i].ItemSpec, e.Message); + return false; + } + } + return true; + } + + /// + /// Remove any output resources that we didn't successfully create (due to error) from the + /// OutputResources list. Keeps the ordering of OutputResources the same. + /// + /// + /// Q: Why didn't we keep a "successfully created" list instead, like in the Copy task does, which + /// would save us doing the removal algorithm below? + /// A: Because we want the ordering of OutputResources to be the same as the ordering passed in. + /// Some items (the up to date ones) would be added to the successful output list first, and the other items + /// are added during processing, so the ordering would change. We could fix that up, but it's better to do + /// the fix up only in the rarer error case. If there were no errors, the algorithm below skips. + private void RemoveUnsuccessfullyCreatedResourcesFromOutputResources() + { + // Normally, there aren't any unsuccessful conversions. + if (_unsuccessfullyCreatedOutFiles == null || + _unsuccessfullyCreatedOutFiles.Count == 0) + { + return; + } + + Debug.Assert(OutputResources != null && OutputResources.Length != 0); + + // We only get here if there was at least one resource generation error. + ITaskItem[] temp = new ITaskItem[OutputResources.Length - _unsuccessfullyCreatedOutFiles.Count]; + int copied = 0; + int removed = 0; + foreach (ITaskItem item in OutputResources) + { + // Check whether this one is in the bad list. + if (removed < _unsuccessfullyCreatedOutFiles.Count && + _unsuccessfullyCreatedOutFiles.Contains(item.ItemSpec)) + { + removed++; + } + else + { + // Copy it to the okay list. + temp[copied] = item; + copied++; + } + } + OutputResources = temp; + } + + /// + /// Read the state file if able. + /// + private void ReadStateFile() + { + // First we look to see if we have a resgen linked files cache. If so, then we can use that + // cache to speed up processing. If there's a problem reading the cache file (or it + // just doesn't exist, then this method will return a brand new cache object. + + // This method eats IO Exceptions + + cache = ResGenDependencies.DeserializeCache(StateFile?.ItemSpec, UseSourcePath, Log); + + //RWOLFF -- throw here? + //ErrorUtilities.VerifyThrow(cache != null, "We did not create a cache!"); + } + + /// + /// Record the list of file that will be written to disk. + /// + private void RecordFilesWritten() + { + // Add any output resources that were successfully created, + // or would have been if they weren't already up to date (important for Clean) + foreach (ITaskItem item in OutputResources) + { + Debug.Assert(File.Exists(item.ItemSpec), item.ItemSpec + " doesn't exist but we're adding to FilesWritten"); + _filesWritten.Add(new TaskItem(item)); + } + + // Add any state file + if (StateFile != null && StateFile.ItemSpec.Length > 0) + { + // It's possible the file wasn't actually written (eg the path was invalid) + // We can't easily tell whether that happened here, and I think it's fine to add it anyway. + _filesWritten.Add(new TaskItem(StateFile)); + } + } + + + /// + /// Determines if the given output file is up to date with respect to the + /// the given input file by comparing timestamps of the two files as well as + /// (if the source is a .resx) the linked files inside the .resx file itself + /// + /// + /// + private bool ShouldRebuildResgenOutputFile(string sourceFilePath, string outputFilePath) + { + bool sourceFileExists = File.Exists(sourceFilePath); + bool destinationFileExists = File.Exists(outputFilePath); + + // PERF: Regardless of whether the outputFile exists, if the source file is a .resx + // go ahead and retrieve it from the cache. This is because we want the cache + // to be populated so that incremental builds can be fast. + // Note that this is a trade-off: clean builds will be slightly slower. However, + // for clean builds we're about to read in this very same .resx file so reading + // it now will page it in. The second read should be cheap. + ResGenDependencies.ResXFile resxFileInfo = null; + if (String.Compare(Path.GetExtension(sourceFilePath), ".resx", true, CultureInfo.InvariantCulture) == 0) + { + try + { + resxFileInfo = cache.GetResXFileInfo(sourceFilePath); + } + catch (ArgumentException) + { + // Return true, so that resource processing will display the error + // No point logging a duplicate error here as well + return true; + } + catch (XmlException) + { + // Return true, so that resource processing will display the error + // No point logging a duplicate error here as well + return true; + } + catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception. + { + ExceptionHandling.RethrowUnlessFileIO(e); + // Return true, so that resource processing will display the error + // No point logging a duplicate error here as well + return true; + } + } + + //////////////////////////////////////////////////////////////////////////////////// + // If the output file does not exist, then we should rebuild it. + // Also, if the input file does not exist, we will also return saying that the + // the output file needs to be rebuilt, so that this pair of files will + // get added to the command-line which will let resgen output whatever error + // it normally outputs in the case when users call the tool with bad params + bool shouldRebuildOutputFile = (!destinationFileExists || !sourceFileExists); + + // if both files do exist, then we need to do some timestamp comparisons + if (!shouldRebuildOutputFile) + { + Debug.Assert(destinationFileExists && sourceFileExists, "GenerateResource task should not check timestamps if neither the .resx nor the .resources files exist"); + + // cache the output file timestamps + DateTime outputFileTimeStamp = File.GetLastWriteTime(outputFilePath); + + // If source file is NOT a .resx, timestamp checking is simple + if (resxFileInfo == null) + { + // We have a non .resx file. Don't attempt to parse it. + + // cache the source file timestamp + DateTime sourceFileTimeStamp = File.GetLastWriteTime(sourceFilePath); + + // we need to rebuild this output file if the source file has a + // more recent timestamp than the output file + shouldRebuildOutputFile = (sourceFileTimeStamp > outputFileTimeStamp); + + return shouldRebuildOutputFile; + } + + // Source file IS a .resx file so we need to do deep dependency analysis + Debug.Assert(resxFileInfo != null, "Why didn't we get resx file information?"); + + // cache the .resx file timestamps + DateTime resxTimeStamp = resxFileInfo.LastModified; + + // we need to rebuild this .resources file if the .resx file has a + // more recent timestamp than the .resources file + shouldRebuildOutputFile = (resxTimeStamp > outputFileTimeStamp); + + // Check the timestamp of each of the passed-in references against the .RESOURCES file. + if (!shouldRebuildOutputFile && (References != null)) + { + foreach (ITaskItem reference in References) + { + // If the reference doesn't exist, then we want to rebuild this + // .resources file so the user sees an error from ResGen.exe + shouldRebuildOutputFile = !File.Exists(reference.ItemSpec); + + // If the reference exists, then we need to compare the timestamp + // for the linked resource to see if it is more recent than the + // .resources file + if (!shouldRebuildOutputFile) + { + DateTime referenceTimeStamp = File.GetLastWriteTime(reference.ItemSpec); + shouldRebuildOutputFile = referenceTimeStamp > outputFileTimeStamp; + } + + // If we found an instance where a reference is in a state + // that we should rebuild the .resources file, then we should + // bail from this loop & just return since the first file that + // forces a rebuild is enough + if (shouldRebuildOutputFile) + { + break; + } + } + } + + // if the .resources is up to date with respect to the .resx file + // then we need to compare timestamps for each linked file inside + // the .resx file itself + if (!shouldRebuildOutputFile && resxFileInfo.LinkedFiles != null) + { + // Linked file paths in the .resx may be relative. Resolve them the same + // way ResXResourceReader does: against BaseLinkedFileDirectory if set, + // otherwise against the directory containing the .resx file. + string linkedFileBaseDir = cache.BaseLinkedFileDirectory ?? Path.GetDirectoryName(sourceFilePath); + + foreach (string rawLinkedFilePath in resxFileInfo.LinkedFiles) + { + string linkedFilePath = Path.IsPathRooted(rawLinkedFilePath) + ? rawLinkedFilePath + : Path.GetFullPath(Path.Combine(linkedFileBaseDir, rawLinkedFilePath)); + + // If the linked file doesn't exist, then we want to rebuild this + // .resources file so the user sees an error from ResGen.exe + shouldRebuildOutputFile = !File.Exists(linkedFilePath); + + // If the linked file exists, then we need to compare the timestamp + // for the linked resource to see if it is more recent than the + // .resources file + if (!shouldRebuildOutputFile) + { + DateTime linkedFileTimeStamp = File.GetLastWriteTime(linkedFilePath); + shouldRebuildOutputFile = linkedFileTimeStamp > outputFileTimeStamp; + } + + // If we found an instance where a linked file is in a state + // that we should rebuild the .resources file, then we should + // bail from this loop & just return since the first file that + // forces a rebuild is enough + if (shouldRebuildOutputFile) + { + break; + } + } + } + } + + return shouldRebuildOutputFile; + //#endif + } + + /// + /// Create the AssemblyName array that ProcessResources will need. + /// + /// AssemblyName array + /// danmose + /// ArgumentException + private AssemblyName[] LoadReferences() + { + if (References == null) + { + return new AssemblyName[0]; + } + + AssemblyName[] assemblyList = new AssemblyName[References.Length]; + + for (int i = 0; i < References.Length; i++) + { + try + { + assemblyList[i] = AssemblyName.GetAssemblyName(References[i].ItemSpec); + } + // We should never get passed in references we can't load. In the VS build process, for example, + // we're passed in @(ReferencePath), which only contains resolved references. + catch (ArgumentNullException e) + { + throw new ArgumentException(e.Message, References[i].ItemSpec); + } + catch (ArgumentException e) + { + throw new ArgumentException(e.Message, References[i].ItemSpec); + } + catch (FileNotFoundException e) + { + throw new ArgumentException(e.Message, References[i].ItemSpec); + } + /*catch (SecurityException e) + { + throw new ArgumentException(e.Message, References[i].ItemSpec); + } + */ + catch (BadImageFormatException e) + { + throw new ArgumentException(e.Message, References[i].ItemSpec); + } + catch (FileLoadException e) + { + throw new ArgumentException(e.Message, References[i].ItemSpec); + } + } + + return assemblyList; + } + + /// + /// Write the state file if there is one to be written. + /// + private void WriteStateFile() + { + if (cache.IsDirty) + { + // And now we serialize the cache to save our resgen linked file resolution for later use. + cache.SerializeCache(StateFile?.ItemSpec, Log); + } + } + } +} diff --git a/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.CrossPlatform.cs b/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.CrossPlatform.cs new file mode 100644 index 0000000..b4ea1fd --- /dev/null +++ b/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.CrossPlatform.cs @@ -0,0 +1,373 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// +// Cross-platform (net8.0+) companion for ProcessResourceFiles. +// +// On .NET Framework the resource pipeline relies on System.Windows.Forms +// (ResXResourceReader) and System.Drawing (GDI+) which are Windows-only and +// unavailable under `dotnet build`. This file provides the cross-platform +// equivalents used when the build task runs on modern .NET: +// +// * NanoResxResourceReader - a focused .resx reader covering the cases the +// nanoFramework pipeline uses (inline strings, ResXFileRef byte[]/string, +// and base64 byte-array blobs). +// * CrossPlatformBitmap - bitmap decoding + RGB565 conversion via ImageSharp, +// producing the exact CLR_GFX_BitmapDescription + payload the nanoCLR expects. +// + +#if !NETFRAMEWORK + +using System; +using System.Collections; +using System.Globalization; +using System.IO; +using System.Resources; +using System.Text; +using System.Xml; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.PixelFormats; + +using BitmapDescription = nanoFramework.Tools.ProcessResourceFiles.NanoResourceFile.CLR_GFX_BitmapDescription; + +namespace nanoFramework.Tools +{ + /// + /// Minimal cross-platform .resx reader. + /// Implements just enough of the .resx schema for the nanoFramework resource + /// pipeline: inline string values, + /// style file references (resolved to byte[] or string), and + /// inline base64 byte arrays. Anything requiring runtime type activation + /// (BinaryFormatter-serialized objects) is reported as unsupported. + /// + internal sealed class NanoResxResourceReader : IResourceReader + { + private const string ResXFileRefTypeMarker = "ResXFileRef"; + private const string ByteArrayMimeType = "application/x-microsoft.net.object.bytearray.base64"; + + private readonly Hashtable _resources = new Hashtable(); + + internal NanoResxResourceReader(TextReader reader, string basePath) + { + Parse(reader, basePath); + } + + private void Parse(TextReader reader, string basePath) + { + XmlDocument doc = new XmlDocument(); + doc.Load(reader); + + XmlNodeList dataNodes = doc.SelectNodes("/root/data"); + if (dataNodes == null) + { + return; + } + + foreach (XmlNode dataNode in dataNodes) + { + string name = dataNode.Attributes?["name"]?.Value; + if (string.IsNullOrEmpty(name)) + { + continue; + } + + string type = dataNode.Attributes?["type"]?.Value; + string mimetype = dataNode.Attributes?["mimetype"]?.Value; + XmlNode valueNode = dataNode.SelectSingleNode("value"); + string rawValue = valueNode != null ? valueNode.InnerText : string.Empty; + + object value = ResolveValue(type, mimetype, rawValue, basePath); + if (value != null) + { + _resources[name] = value; + } + } + } + + private object ResolveValue(string type, string mimetype, string rawValue, string basePath) + { + // Inline base64 byte array (e.g. small binary blobs embedded in the .resx). + if (!string.IsNullOrEmpty(mimetype)) + { + if (string.Equals(mimetype, ByteArrayMimeType, StringComparison.OrdinalIgnoreCase)) + { + return Convert.FromBase64String(rawValue.Trim()); + } + + throw new NotSupportedException( + $"Resource mimetype '{mimetype}' is not supported when building with 'dotnet build'. " + + "Use full .NET Framework MSBuild / Visual Studio for this resource."); + } + + // File reference (ResXFileRef): ";[;]". + if (!string.IsNullOrEmpty(type) && type.Contains(ResXFileRefTypeMarker)) + { + return ResolveFileRef(rawValue, basePath); + } + + // Anything else with an explicit non-primitive type is not supported here. + if (!string.IsNullOrEmpty(type) && !IsStringType(type)) + { + throw new NotSupportedException( + $"Resource type '{type}' is not supported when building with 'dotnet build'. " + + "Use full .NET Framework MSBuild / Visual Studio for this resource."); + } + + // Plain inline string. + return rawValue; + } + + private object ResolveFileRef(string fileRef, string basePath) + { + // Format: filePath;typeName[;encoding] + string[] parts = SplitFileRef(fileRef); + string path = parts[0].Trim(); + string typeName = parts.Length > 1 ? parts[1].Trim() : "System.Byte[]"; + + string resolvedPath = Path.IsPathRooted(path) + ? path + : Path.GetFullPath(Path.Combine(basePath ?? Directory.GetCurrentDirectory(), path)); + + if (IsStringType(typeName)) + { + Encoding encoding = Encoding.UTF8; + if (parts.Length > 2 && !string.IsNullOrWhiteSpace(parts[2])) + { + try + { + encoding = Encoding.GetEncoding(parts[2].Trim()); + } + catch (ArgumentException) + { + // fall back to UTF-8 + } + } + + return File.ReadAllText(resolvedPath, encoding); + } + + if (IsMemoryStreamType(typeName)) + { + return new MemoryStream(File.ReadAllBytes(resolvedPath)); + } + + // Default (and the common nanoFramework case): raw bytes (System.Byte[]). + return File.ReadAllBytes(resolvedPath); + } + + /// + /// Splits a ResXFileRef value into [path, type, encoding]. The path may itself + /// contain ';' so we only split on the separators that precede the type/encoding. + /// + private static string[] SplitFileRef(string fileRef) + { + // ResXFileRef.Parse splits on ';' into at most 3 segments, but a quoted path + // ("path";type;encoding) protects a path containing ';'. Handle the quoted form. + fileRef = fileRef.Trim(); + if (fileRef.StartsWith("\"")) + { + int closingQuote = fileRef.IndexOf('"', 1); + if (closingQuote > 0) + { + string path = fileRef.Substring(1, closingQuote - 1); + string remainder = fileRef.Substring(closingQuote + 1).TrimStart(';'); + string[] rest = remainder.Split(';'); + string[] result = new string[1 + rest.Length]; + result[0] = path; + Array.Copy(rest, 0, result, 1, rest.Length); + return result; + } + } + + return fileRef.Split(';'); + } + + private static bool IsStringType(string typeName) + { + return typeName != null && typeName.StartsWith("System.String", StringComparison.Ordinal); + } + + private static bool IsMemoryStreamType(string typeName) + { + return typeName != null && typeName.StartsWith("System.IO.MemoryStream", StringComparison.Ordinal); + } + + #region IResourceReader / IEnumerable / IDisposable + + public IDictionaryEnumerator GetEnumerator() + { + return _resources.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _resources.GetEnumerator(); + } + + public void Close() + { + _resources.Clear(); + } + + public void Dispose() + { + Close(); + } + + #endregion + } + + /// + /// Cross-platform bitmap handling for the nanoFramework resource pipeline, + /// backed by ImageSharp. Mirrors the .NET Framework / GDI+ behaviour: + /// BMP images are converted to uncompressed 16bpp RGB565; JPEG and GIF + /// images are stored as-is with the matching bitmap type. + /// + internal static class CrossPlatformBitmap + { + /// + /// Returns true if the raw bytes decode as one of the bitmap formats the + /// nanoFramework runtime supports (BMP, GIF, JPEG). PNG and other formats + /// are intentionally rejected so they are treated as binary resources, + /// matching the .NET Framework implementation. + /// + internal static bool IsSupportedBitmap(byte[] rawValue) + { + return TryGetBitmapType(rawValue, out _); + } + + internal static byte[] GenerateBitmapResourceData(byte[] rawValue) + { + if (!TryGetBitmapType(rawValue, out byte bitmapType)) + { + throw new NotSupportedException("Bitmap format not supported."); + } + + using (Image image = Image.Load(rawValue)) + { + if (image.Width > 0xFFFF || image.Height > 0xFFFF) + { + throw new ArgumentException("bitmap dimensions out of range"); + } + + BitmapDescription description; + byte[] data; + + if (bitmapType == BitmapDescription.c_TypeBitmap) + { + // BMP -> uncompressed 16bpp RGB565. + data = ConvertToRgb565(image); + description = new BitmapDescription( + (ushort)image.Width, + (ushort)image.Height, + 0, + 16, + BitmapDescription.c_TypeBitmap); + } + else + { + // JPEG / GIF -> stored encoded, decoded by the device at runtime. + data = rawValue; + description = new BitmapDescription( + (ushort)image.Width, + (ushort)image.Height, + 0, + 1, + bitmapType); + } + + using (MemoryStream stream = new MemoryStream()) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + description.Serialize(writer); + writer.Write(data); + writer.Flush(); + return stream.ToArray(); + } + } + } + + private static bool TryGetBitmapType(byte[] rawValue, out byte bitmapType) + { + bitmapType = 0; + + if (rawValue == null || rawValue.Length == 0) + { + return false; + } + + try + { + IImageFormat format = Image.DetectFormat(rawValue); + if (format == null) + { + return false; + } + + switch (format.Name.ToUpperInvariant()) + { + case "BMP": + bitmapType = BitmapDescription.c_TypeBitmap; + return true; + case "GIF": + bitmapType = BitmapDescription.c_TypeGif; + return true; + case "JPEG": + bitmapType = BitmapDescription.c_TypeJpeg; + return true; + default: + return false; + } + } + catch + { + return false; + } + } + + /// + /// Converts an image to contiguous 16bpp RGB565, row-major, top-down, + /// little-endian per pixel — the layout the nanoCLR expects (and that + /// GDI+ Format16bppRgb565 produces for even widths). + /// + private static byte[] ConvertToRgb565(Image image) + { + int width = image.Width; + int height = image.Height; + byte[] data = new byte[width * height * 2]; + + image.ProcessPixelRows(accessor => + { + int offset = 0; + for (int y = 0; y < height; y++) + { + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Rgba32 p = row[x]; + + // Quantize 8-bit channels to RGB565. Green/blue match GDI+ + // (the net472 System.Drawing path) exactly. Red uses round-to- + // nearest, which matches GDI+ except for colors that land exactly + // on a half-level boundary: there GDI+ applies ordered (Bayer) + // dithering by pixel position, which is NOT reproduced here. Such + // pixels can differ by 1 LSB of red from the legacy/net472 output. + // See SmokeTest Resources.resx parity notes. + int r5 = Math.Min(31, (p.R + 4) >> 3); + int g6 = p.G >> 2; + int b5 = Math.Min(31, (p.B + 4) >> 3); + + ushort value = (ushort)((r5 << 11) | (g6 << 5) | b5); + data[offset++] = (byte)(value & 0xFF); + data[offset++] = (byte)(value >> 8); + } + } + }); + + return data; + } + } +} + +#endif diff --git a/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.cs b/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.cs new file mode 100644 index 0000000..58ab97a --- /dev/null +++ b/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.cs @@ -0,0 +1,2168 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// +// ****************************************************************************** +// ** IMPORTANT: THIS FILE IS DUPLICATED. ** +// ** ** +// ** The canonical .NET Framework copy lives in the VS extension repo: ** +// ** nf-Visual-Studio-extension / Tools.BuildTasks-2019 / ** +// ** ProcessResourceFiles.cs ** +// ** ** +// ** This copy adds #if NETFRAMEWORK guards for cross-platform (net8.0+) ** +// ** support. Any logic change made here MUST be ported there, and vice-versa.** +// ****************************************************************************** + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System; +using System.CodeDom; +using System.CodeDom.Compiler; +using System.Collections; +using System.Diagnostics; +#if NETFRAMEWORK +using System.Drawing; +using System.Drawing.Imaging; +#endif +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Resources; +using System.Runtime.Serialization; +using System.Text; +using System.Xml; + +namespace nanoFramework.Tools +{ + + /// + /// This class handles the processing of source resource files into compiled resource files. + /// Its designed to be called from a separate AppDomain so that any files locked by ResXResourceReader + /// can be released. + /// + internal sealed class ProcessResourceFiles : MarshalByRefObject + { + + #region fields + /// + /// Resource list (used to preserve resource ordering, primarily for easier testing) + /// + private ArrayList resources = new ArrayList(); + + /// + /// Mirror resource list, used to check for duplicates + /// + private Hashtable resourcesHashTable = new Hashtable(StringComparer.InvariantCultureIgnoreCase); + + /// + /// Logger for any messages or errors + /// + private TaskLoggingHelper logger = null; + + public string StronglyTypedNamespace { get; set; } + + /// + /// Class name for the strongly typed resources. + /// Getter provided since the processor may choose a default. + /// + public string StronglyTypedClassName { get; set; } + + /// + /// List of assemblies to use for type resolution within resx files + /// + private AssemblyName[] assemblyList; + + /// + /// List of input files to process. + /// + private ITaskItem[] inFiles; + + /// + /// List of output files to process. + /// + private ITaskItem[] outFiles; + + public bool GenerateNestedEnums { get; set; } = true; + + public bool GenerateInternalClass { get; set; } = true; + + public bool IsMscorlib { get; set; } = false; + + + /// + /// List of output files that we failed to create due to an error. + /// See note in RemoveUnsuccessfullyCreatedResourcesFromOutputResources() + /// + internal ArrayList UnsuccessfullyCreatedOutFiles + { + get + { + if (null == unsuccessfullyCreatedOutFiles) + { + unsuccessfullyCreatedOutFiles = new ArrayList(); + } + return unsuccessfullyCreatedOutFiles; + } + } + private ArrayList unsuccessfullyCreatedOutFiles; + + /// + /// Whether we successfully created the STR class + /// + internal bool StronglyTypedResourceSuccessfullyCreated { get; private set; } = false; + + /// + /// Indicates whether the resource reader should use the source file's + /// directory to resolve relative file paths. + /// + private bool useSourcePath = false; + + private ITaskItem _inTaskItem; + private ITaskItem _outTaskItem; + + #endregion + + /// + /// Process all files. + /// + internal void Run(TaskLoggingHelper log, AssemblyName[] assemblies, ITaskItem[] inputs, ITaskItem[] outputs, bool sourcePath) + { + logger = log; + assemblyList = assemblies; + inFiles = inputs; + outFiles = outputs; + useSourcePath = sourcePath; + + for (int i = 0; i < inFiles.Length; ++i) + { + if (!ProcessFile(inFiles[i], outFiles[i])) + { + UnsuccessfullyCreatedOutFiles.Add(outFiles[i]); + } + } + } + + private void Init() + { + resources.Clear(); + resourcesHashTable.Clear(); + } + + private void InitFileProcessing(ITaskItem inTaskItem, ITaskItem outTaskItem) + { + Init(); + + _inTaskItem = inTaskItem; + _outTaskItem = outTaskItem; + } + + #region Code from ResGen.EXE + /// + /// Read all resources from a file and write to a new file in the chosen format + /// + /// Uses the input and output file extensions to determine their format + /// Input resources file + /// Output resources file + /// True if conversion was successful, otherwise false + private bool ProcessFile(ITaskItem inTaskItem, ITaskItem outTaskItem) + { + InitFileProcessing(inTaskItem, outTaskItem); + + string inFile = inTaskItem.ItemSpec; + string outFile = outTaskItem.ItemSpec; + + if (GetFormat(inFile) == Format.Error) + { + logger.LogError("GenerateResource.UnknownFileExtension", Path.GetExtension(inFile), inFile); + return false; + } + if (GetFormat(outFile) == Format.Error) + { + logger.LogError("GenerateResource.UnknownFileExtension", Path.GetExtension(outFile), outFile); + return false; + } + + // logger.LogMessage("GenerateResource.ProcessingFile", inFile, outFile); + + try + { + ReadResources(inFile, useSourcePath); + } + catch (ArgumentException ae) + { + if (ae.InnerException is XmlException) + { + XmlException xe = (XmlException)ae.InnerException; + logger.LogError(null, inFile, xe.LineNumber, xe.LinePosition, 0, 0, "General.InvalidResxFile", xe.Message); + } + else + { + logger.LogError(null, inFile, 0, 0, 0, 0, "General.InvalidResxFile", ae.Message); + } + return false; + } + catch (XmlException xe) + { + logger.LogError(null, inFile, xe.LineNumber, xe.LinePosition, 0, 0, "General.InvalidResxFile", xe.Message); + return false; + } +#if MSBUILD_SOURCES + catch (Exception e) + { + ExceptionHandling.RethrowUnlessFileIO(e); + logger.LogError(null, inFile, 0, 0, 0, 0, "General.InvalidResxFile", e.Message); + + // We need to give meaningful error messages to the user. + // Note that ResXResourceReader wraps any exception it gets + // in an ArgumentException with the message "Invalid ResX input." + // If you don't look at the InnerException, you have to attach + // a debugger to find the problem. + if (e.InnerException != null) + { + Exception inner = e.InnerException; + StringBuilder sb = new StringBuilder(200); + sb.Append(e.Message); + while (inner != null) + { + sb.Append(" ---> "); + sb.Append(inner.GetType().Name); + sb.Append(": "); + sb.Append(inner.Message); + inner = inner.InnerException; + } + logger.LogError(null, inFile, 0, 0, 0, 0, "General.InvalidResxFile", sb.ToString()); + } + return false; + } +#endif + catch + { + throw; + } + + try + { + WriteResources(outFile); + } + catch (IOException io) + { + logger.LogError("GenerateResource.CannotWriteOutput", outFile, io.Message); + + if (File.Exists(outFile)) + { + logger.LogError("GenerateResource.CorruptOutput", outFile); + try + { + File.Delete(outFile); + } + catch (Exception e) + { + logger.LogWarning("GenerateResource.DeleteCorruptOutputFailed", outFile, e.Message); + } + } + return false; + } +#if MSBUILD_SOURCES + catch (Exception e) + { + ExceptionHandling.RethrowUnlessFileIO(e); + logger.LogError("GenerateResource.CannotWriteOutput", outFile, e.Message); + return false; + } +#endif + catch + { + throw; + } + + return true; + } + + /// + /// Figure out the format of an input resources file from the extension + /// + /// Input resources file + /// Resources format + private Format GetFormat(string filename) + { + string extension = Path.GetExtension(filename); + if (String.Compare(extension, ".txt", true, CultureInfo.InvariantCulture) == 0 || + String.Compare(extension, ".restext", true, CultureInfo.InvariantCulture) == 0) + { + return Format.Text; + } + else if (String.Compare(extension, ".resx", true, CultureInfo.InvariantCulture) == 0) + { + return Format.XML; + } + else if (String.Compare(extension, ".resources", true, CultureInfo.InvariantCulture) == 0) + { + return Format.Binary; + } + else if (String.Compare(extension, ".nanoresources", true, CultureInfo.InvariantCulture) == 0) + { + return Format.NanoResources; + } + else + { + return Format.Error; + } + } + + /// + /// Text files are just name/value pairs. ResText is the same format + /// with a unique extension to work around some ambiguities with MSBuild + /// ResX is our existing XML format from V1. + /// + private enum Format + { + Text, // .txt or .restext + XML, // .resx + Binary, // .resources + NanoResources, //.nanoresources + Error, // anything else + } + + /// + /// Reads the resources out of the specified file and populates the + /// resources hashtable. + /// + /// Filename to load + /// Whether to resolve paths in the + /// resources file relative to the resources file location + public void ReadResources(String filename, bool shouldUseSourcePath) + { + // Reset state + // resources.Clear(); + // resourcesHashTable.Clear(); + + Format format = GetFormat(filename); + switch (format) + { + case Format.Text: + ReadTextResources(filename); + break; + + case Format.XML: +#if NETFRAMEWORK + ResXResourceReader resXReader = null; + if (assemblyList != null) + { + resXReader = new ResXResourceReader(filename, assemblyList); + } + else + { + resXReader = new ResXResourceReader(filename); + } + if (shouldUseSourcePath) + { + String fullPath = Path.GetFullPath(filename); + resXReader.BasePath = Path.GetDirectoryName(fullPath); + } + // ReadResources closes the reader for us + ReadResources(resXReader, filename); +#else + { + string basePath = shouldUseSourcePath + ? Path.GetDirectoryName(Path.GetFullPath(filename)) + : null; + using (StreamReader sr = new StreamReader(filename)) + { + ReadResources(new NanoResxResourceReader(sr, basePath), filename); + } + } +#endif + break; + + case Format.Binary: + ReadResources(new ResourceReader(filename), filename); // closes reader for us + break; + case Format.NanoResources: + Debug.Fail("Unknown format " + format.ToString()); + break; + + default: + // We should never get here, we've already checked the format + Debug.Fail("Unknown format " + format.ToString()); + return; + } + //logger.LogMessage(BuildEventImportance.Low/*MessageImportance.Low*/, "GenerateResource.ReadResourceMessage", resources.Count, filename); + } + + /// + /// Write resources from the resources ArrayList to the specified output file + /// + /// Output resources file + public void WriteResources(String filename) + { + Format format = GetFormat(filename); + switch (format) + { + case Format.Text: + WriteTextResources(filename); + break; + + case Format.XML: +#if NETFRAMEWORK + WriteResources(new ResXResourceWriter(filename)); // closes writer for us + break; +#else + throw new NotSupportedException("Writing .resx output is not supported on this build host."); +#endif + + case Format.Binary: + WriteResources(new ResourceWriter(filename)); // closes writer for us + break; + + case Format.NanoResources: + WriteResources(new NanoResourceWriter(filename)); // closes writer for us + break; + default: + // We should never get here, we've already checked the format + Debug.Fail("Unknown format " + format.ToString()); + break; + } + } + + private void CreateStronglyTypedResources(CodeDomProvider provider, TextWriter writer, string resourceName, out string[] errors) + { + CodeCompileUnit ccu = CreateStronglyTypedResourceFile(resourceName, resources, + StronglyTypedClassName, StronglyTypedNamespace, provider, out errors); + CodeGeneratorOptions codeGenOptions = new CodeGeneratorOptions(); + codeGenOptions.BlankLinesBetweenMembers = false; + codeGenOptions.BracingStyle = "C"; + + provider.GenerateCodeFromCompileUnit(ccu, writer, codeGenOptions); + writer.Flush(); + } + + public void CreateStronglyTypedResources(string inputFileName, CodeDomProvider provider, TextWriter writer, string resourceName) + { + Init(); + + ReadResources(inputFileName, true); + + string[] errors = null; + + CreateStronglyTypedResources(provider, writer, resourceName, out errors); + + if (errors != null && errors.Length > 0) + { + throw new ApplicationException(errors[0]); + } + + StronglyTypedResourceSuccessfullyCreated = true; + } + + /// + /// Generates a strongly typed resource class from in-memory .resx content. + /// Used by the VS custom tool so that unsaved designer edits are reflected + /// immediately without waiting for the file to be written to disk. + /// + public void CreateStronglyTypedResources(string inputFileName, string inputFileContent, CodeDomProvider provider, TextWriter writer, string resourceName) + { + Init(); + + ReadResources(inputFileName, inputFileContent, true); + + string[] errors = null; + + CreateStronglyTypedResources(provider, writer, resourceName, out errors); + + if (errors != null && errors.Length > 0) + { + throw new ApplicationException(errors[0]); + } + + StronglyTypedResourceSuccessfullyCreated = true; + } + + /// + /// Reads resources from in-memory .resx content. For ResXFileRef entries the + /// linked files are still resolved from disk using the directory of + /// as the base path. + /// + public void ReadResources(String filename, string fileContent, bool shouldUseSourcePath) + { + Format format = GetFormat(filename); + if (format == Format.XML) + { + string basePath = shouldUseSourcePath + ? Path.GetDirectoryName(Path.GetFullPath(filename)) + : null; +#if NETFRAMEWORK + ResXResourceReader resXReader = assemblyList != null + ? new ResXResourceReader(new StringReader(fileContent), assemblyList) + : new ResXResourceReader(new StringReader(fileContent)); + if (basePath != null) + { + resXReader.BasePath = basePath; + } + ReadResources(resXReader, filename); +#else + using (StringReader sr = new StringReader(fileContent)) + { + ReadResources(new NanoResxResourceReader(sr, basePath), filename); + } +#endif + } + else + { + ReadResources(filename, shouldUseSourcePath); + } + } + + private CodeNamespace CreateNamespace(CodeCompileUnit ccu, string ns, Hashtable tableNamespaces) + { + CodeNamespace codeNamespace = (CodeNamespace)tableNamespaces[ns]; + + if (codeNamespace == null) + { + codeNamespace = new CodeNamespace(ns); + ccu.Namespaces.Add(codeNamespace); + tableNamespaces[ns] = codeNamespace; + } + + return codeNamespace; + } + + private CodeTypeDeclaration CreateTypeDeclaration(CodeNamespace codeNamespace, string type, Hashtable tableTypes) + { + CodeTypeDeclaration codeTypeDeclaration = (CodeTypeDeclaration)tableTypes[type]; + + if (codeTypeDeclaration == null) + { + int iPlus = type.LastIndexOf('+'); + + if (iPlus < 0) + { + codeTypeDeclaration = new CodeTypeDeclaration(type); + codeTypeDeclaration.IsPartial = true; + + codeNamespace.Types.Add(codeTypeDeclaration); + } + else + { + string typeBase = type.Substring(0, iPlus); + string typeNested = type.Substring(iPlus + 1); + + CodeTypeDeclaration codeTypeDeclarationBase = CreateTypeDeclaration(codeNamespace, typeBase, tableTypes); + codeTypeDeclaration = new CodeTypeDeclaration(typeNested); + + codeTypeDeclarationBase.Members.Add(codeTypeDeclaration); + } + + MakeInternalIfNecessary(codeTypeDeclaration); + tableTypes[type] = codeTypeDeclaration; + } + + return codeTypeDeclaration; + } + + private void CreateHelperMethod(CodeTypeDeclaration codeTypeDeclaration, Entry.ResourceTypeDescription typeDesciption, string parameterType) + { + + /* + public static ( id) + { + return ().GetObject( .ResourceManager, id ); + } + + for example + + public static Font GetFont( FontTag id ) + { + return (Font)Microsoft.SPOT.ResourcesUtility.GetObject( MyResources.ResourceManager, FontTag id ); + } + + */ + + CodeMemberMethod method = new CodeMemberMethod(); + CodeParameterDeclarationExpression parameterIdDeclaration = new CodeParameterDeclarationExpression(parameterType, "id"); + CodeVariableReferenceExpression parameterIdReference = new CodeVariableReferenceExpression(parameterIdDeclaration.Name); + codeTypeDeclaration.Members.Add(method); + + method.Name = typeDesciption.helperName; + method.Parameters.Add(parameterIdDeclaration); + method.ReturnType = new CodeTypeReference(typeDesciption.runtimeType); + + method.Attributes = MemberAttributes.Public | MemberAttributes.Static; + MakeInternalIfNecessary(method); + + string getObjectClass = IsMscorlib ? "System.Resources.ResourceManager" : "nanoFramework.Runtime.Native.ResourceUtility"; + + CodeVariableReferenceExpression expresionId = new CodeVariableReferenceExpression("id"); + CodeTypeReferenceExpression spotResourcesReference = new CodeTypeReferenceExpression(getObjectClass); + CodePropertyReferenceExpression resourceManagerReference = new CodePropertyReferenceExpression(null, "ResourceManager"); + CodeExpression expressionGetObject = new CodeMethodInvokeExpression(spotResourcesReference, "GetObject", resourceManagerReference, expresionId); + CodeExpression expressionValue = new CodeCastExpression(typeDesciption.runtimeType, expressionGetObject); + CodeMethodReturnStatement statementReturn = new CodeMethodReturnStatement(expressionValue); + method.Statements.Add(statementReturn); + } + + private void MakeInternalIfNecessary(CodeTypeDeclaration codeTypeDeclaration) + { + if (GenerateInternalClass) + { + codeTypeDeclaration.TypeAttributes &= ~TypeAttributes.VisibilityMask; + codeTypeDeclaration.TypeAttributes |= TypeAttributes.NestedAssembly; + } + } + + private void MakeInternalIfNecessary(CodeTypeMember codeTypeMember) + { + if (GenerateInternalClass) + { + codeTypeMember.Attributes &= ~MemberAttributes.AccessMask; + codeTypeMember.Attributes |= MemberAttributes.Assembly; + } + } + + + private CodeCompileUnit CreateStronglyTypedResourceFile(string resourceName, ArrayList resources, string className, string ns, CodeDomProvider provider, out string[] errors) + { + //create list of classes needed to be emitted. + CodeCompileUnit ccu = new CodeCompileUnit(); + + Hashtable tableNamespaces = new Hashtable(); + Hashtable tableTypes = new Hashtable(); + Hashtable tableHelperFunctionsNeeded = new Hashtable(); + ArrayList[] resourceTypesUsed = new ArrayList[NanoResourceFile.ResourceHeader.RESOURCE_Max + 1]; + + CodeNamespace codeNamespace; + CodeTypeDeclaration codeTypeDeclaration; + + //break down resources by enum + for (int iEntry = 0; iEntry < resources.Count; iEntry++) + { + Entry entry = (Entry)resources[iEntry]; + + if (resourceTypesUsed[entry.ResourceType] == null) + { + resourceTypesUsed[entry.ResourceType] = new ArrayList(); + } + + if (!resourceTypesUsed[entry.ResourceType].Contains(entry.ClassName)) + { + resourceTypesUsed[entry.ResourceType].Add(entry.ClassName); + } + + codeNamespace = CreateNamespace(ccu, entry.Namespace, tableNamespaces); + codeTypeDeclaration = CreateTypeDeclaration(codeNamespace, entry.ClassName, tableTypes); + + if (!codeTypeDeclaration.IsEnum) + { + //only initialize once + codeTypeDeclaration.IsEnum = true; + codeTypeDeclaration.CustomAttributes.Add(new CodeAttributeDeclaration("System.SerializableAttribute")); + codeTypeDeclaration.BaseTypes.Add(new CodeTypeReference(typeof(short))); + } + + CodeMemberField codeMemberField = new CodeMemberField(entry.ClassName, entry.Field); + codeMemberField.Attributes = MemberAttributes.Const | MemberAttributes.Static; + + CodePrimitiveExpression codeExpression = new CodePrimitiveExpression(entry.Id); + codeMemberField.InitExpression = codeExpression; + //set constant value??!! + int iField = codeTypeDeclaration.Members.Add(codeMemberField); + + } + + //emit helper functions + + codeNamespace = CreateNamespace(ccu, ns, tableNamespaces); + codeTypeDeclaration = CreateTypeDeclaration(codeNamespace, className, tableTypes); + MakeInternalIfNecessary(codeTypeDeclaration); + + CodeTypeReferenceExpression codeTypeReferenceExpression = new CodeTypeReferenceExpression(codeTypeDeclaration.Name); + + //private static System.Resources.ResourceManager manager; + CodeMemberField fieldManager = new CodeMemberField("System.Resources.ResourceManager", "manager"); + CodeFieldReferenceExpression fieldManagerExpression = new CodeFieldReferenceExpression(codeTypeReferenceExpression, fieldManager.Name); + + fieldManager.Attributes = MemberAttributes.Static | MemberAttributes.Private; + codeTypeDeclaration.Members.Add(fieldManager); + + /* + [private|internal] static System.Resources.ResourceManager ResourceManager + { + get + { + if(manager == null) + { + manager = new System.Resources.ResourceManager( "Resources", typeof( Resources ).Assembly ); + } + + return manager; + } + } + */ + + CodeBinaryOperatorExpression getResourceManagerExpressionIfNull = new CodeBinaryOperatorExpression(fieldManagerExpression, CodeBinaryOperatorType.IdentityEquality, new CodePrimitiveExpression(null)); + CodeObjectCreateExpression getResourceManagerExpressionNewManager = new CodeObjectCreateExpression( + "System.Resources.ResourceManager", + new CodePrimitiveExpression(resourceName), + new CodePropertyReferenceExpression(new CodeTypeOfExpression(codeTypeReferenceExpression.Type), "Assembly") + ); + CodeAssignStatement getResourceManagerExpressionInitializeManager = new CodeAssignStatement(fieldManagerExpression, getResourceManagerExpressionNewManager); + + CodeStatementCollection getResourceManagerExpression = new CodeStatementCollection(); + + getResourceManagerExpression.AddRange(new CodeStatement[] { + new CodeConditionStatement( getResourceManagerExpressionIfNull, getResourceManagerExpressionInitializeManager ), + new CodeMethodReturnStatement( fieldManagerExpression ) + } + ); + + CodeMemberProperty propertyResourceManager = new CodeMemberProperty(); + propertyResourceManager.Name = "ResourceManager"; + propertyResourceManager.Type = new CodeTypeReference("System.Resources.ResourceManager"); + propertyResourceManager.Attributes = MemberAttributes.Public | MemberAttributes.Static; + MakeInternalIfNecessary(propertyResourceManager); + propertyResourceManager.GetStatements.AddRange(getResourceManagerExpression); + codeTypeDeclaration.Members.Add(propertyResourceManager); + + for (int i = 0; i < resourceTypesUsed.Length; i++) + { + ArrayList list = resourceTypesUsed[i]; + + if (list != null) + { + Entry.ResourceTypeDescription typeDescription = Entry.ResourceTypeDescriptionFromResourceType((byte)i); + + for (int iClassName = 0; iClassName < list.Count; iClassName++) + { + CreateHelperMethod(codeTypeDeclaration, typeDescription, (string)list[iClassName]); + } + } + } + + errors = new string[0]; + return ccu; + } + /// + /// If no strongly typed resource class filename was specified, we come up with a default based on the + /// input file name and the default language extension. Broken out here so it can be called from GenerateResource class. + /// + /// A CodeDomProvider for the language + /// Name of the output resources file + /// Filename for strongly typed resource class + public static string GenerateDefaultStronglyTypedFilename(CodeDomProvider provider, string outputResourcesFile) + { + return Path.ChangeExtension(outputResourcesFile, provider.FileExtension); + } + + /// + /// Read resources from an XML or binary format file + /// + /// Appropriate IResourceReader + /// Filename, for error messages + private void ReadResources(IResourceReader reader, String fileName) + { + using (reader) + { + IDictionaryEnumerator resEnum = reader.GetEnumerator(); + while (resEnum.MoveNext()) + { + string name = (string)resEnum.Key; + // Replace dot in the name with underscore. + // 1. First reason - this is what desktop resource generator does. + // 2. Second reason - Extra dots causes resource generator to create name space and enumerations. + // This complicates the syntax and finally create invalid code if 2 or more dots are present. + // So we just make longer name. + name = name.Replace('.', '_'); + object value; + try + { + value = resEnum.Value; + } + catch (Exception ex) + { + logger?.LogWarning(null, fileName, 0, 0, 0, 0, "GenerateResource.CannotLoadResource", (string)resEnum.Key, ex.Message); + continue; + } + AddResource(name, value, fileName); + } + } + + EnsureResourcesIds(resources); + } + + private static short GenerateIdFromResourceName(string s) + { + //adapted from BCL implementation + + int hash1 = (5381 << 16) + 5381; + int hash2 = hash1; + + char[] chars = s.ToCharArray(); + + int len = s.Length; + + for (int i = 0; i < len; i++) + { + char c = s[i]; + if (i % 2 == 0) + { + hash1 = ((hash1 << 5) + hash1) ^ c; + } + else + { + hash2 = ((hash2 << 5) + hash2) ^ c; + } + } + + int hash = hash1 + (hash2 * 1566083941); + + short ret = (short)((short)(hash >> 16) ^ (short)hash); + + return ret; + } + + internal static void EnsureResourcesIds(ArrayList resources) + { + int iResource; + Hashtable table = new Hashtable(); + + if (resources.Count > UInt16.MaxValue) + { + throw new ApplicationException("Too many resources. Maximum number of resources per ResourceSet is 65535"); + } + + for (iResource = 0; iResource < resources.Count; iResource++) + { + Entry entry = (Entry)resources[iResource]; + + short id = entry.Id; + + if (table.ContainsKey(id)) + { + //rwolff -- check regarding boxed objects.... + + //duplicate id detected. + Entry entryDup = (Entry)table[id]; + + throw new ApplicationException(string.Format("Duplicate id detected. Resources '{0}' and '{1}' are generating the same id=0x{2}", entry.Name, entryDup.Name, id)); + } + + table[id] = entry; + } + + resources.Sort(); + } + + /// + /// Read resources from a text format file + /// + /// Input resources filename + private void ReadTextResources(String fileName) + { + throw new NotImplementedException(); + } + + /// + /// Write resources to an XML or binary format resources file. + /// + /// Closes writer automatically + /// Appropriate IResourceWriter + private void WriteResources(IResourceWriter writer) + { + try + { + foreach (Entry entry in resources) + { + string key = entry.RawName; + object value = entry.Value; + + if (writer is NanoResourceWriter) + { + ((NanoResourceWriter)writer).AddResource(entry); + } + else + { + writer.AddResource(key, value); + } + } + + writer.Generate(); + } + finally + { + writer.Close(); + } + } + + /// + /// Write resources to a text format resources file + /// + /// Output resources file + private void WriteTextResources(String fileName) + { + using (StreamWriter writer = new StreamWriter(fileName, false, Encoding.UTF8)) + { + foreach (Entry entry in resources) + { + String key = entry.Name; + Object v = entry.Value; + String value = v as String; + if (value == null) + { + logger.LogError(null, fileName, 0, 0, 0, 0, "GenerateResource.OnlyStringsSupported", key, v.GetType().FullName); + } + else + { + // Escape any special characters in the String. + value = value.Replace("\\", "\\\\"); + value = value.Replace("\n", "\\n"); + value = value.Replace("\r", "\\r"); + value = value.Replace("\t", "\\t"); + + writer.WriteLine("{0}={1}", key, value); + } + } + } + } + + /// + /// Add a resource from a text file to the internal data structures + /// + /// Resource name + /// Resource value + /// Input file for messages + /// Line number for messages + /// Column number for messages + private void AddResource(string name, object value, String inputFileName, int lineNumber, int linePosition) + { + if (resourcesHashTable.ContainsKey(name)) + { + logger?.LogWarning(null, inputFileName, lineNumber, linePosition, 0, 0, "GenerateResource.DuplicateResourceName", name); + return; + } + + if (value == null) + { + // ResXResourceReader returns a null value when a resource cannot be materialized + // (e.g. a 'bytearray.base64' mimetype with no declared 'type' attribute). Skip it + // with a clear warning instead of failing the whole build with an opaque NRE. + logger?.LogWarning($"Resource '{name}' in '{inputFileName}' could not be loaded (value is null; missing or unsupported type/mimetype) and was skipped."); + return; + } + + Entry entry = Entry.CreateEntry(name, value, StronglyTypedNamespace, GenerateNestedEnums ? StronglyTypedClassName : string.Empty); + + Debug.Assert(entry.ClassName.Length > 0); + + resources.Add(entry); + resourcesHashTable[name] = entry; + } + + private void AddResource(string name, object value, String inputFileName) + { + AddResource(name, value, inputFileName, 0, 0); + } + + /// + /// Name value resource pair to go in resources list + /// + internal abstract class Entry : IComparable + { + public class ResourceTypeDescription + { + public byte resourceType; + public string helperName; + public string runtimeType; + public string defaultEnum; + + public ResourceTypeDescription(byte resourceType, string helperName, string runtimeType, string defaultEnum) + { + this.resourceType = resourceType; + this.helperName = helperName; + this.runtimeType = runtimeType; + this.defaultEnum = defaultEnum; + } + } + + private static ResourceTypeDescription[] typeDescriptions = new ResourceTypeDescription[] + { + null, //RESOURCE_Invalid + new ResourceTypeDescription(NanoResourceFile.ResourceHeader.RESOURCE_Bitmap, "GetBitmap", "nanoFramework.UI.Bitmap", "BitmapResources"), + new ResourceTypeDescription(NanoResourceFile.ResourceHeader.RESOURCE_Font, "GetFont", "nanoFramework.UI.Font", "FontResources"), + new ResourceTypeDescription(NanoResourceFile.ResourceHeader.RESOURCE_String, "GetString", "System.String", "StringResources"), + new ResourceTypeDescription(NanoResourceFile.ResourceHeader.RESOURCE_Binary, "GetBytes", "System.Byte[]", "BinaryResources"), + }; + + public static ResourceTypeDescription ResourceTypeDescriptionFromResourceType(byte type) + { + if (type < NanoResourceFile.ResourceHeader.RESOURCE_Bitmap || type > NanoResourceFile.ResourceHeader.RESOURCE_Binary) + { + throw new ArgumentException(); + } + + return typeDescriptions[type]; + } + + public static Entry CreateEntry(string name, object value, string defaultNamespace, string defaultDeclaringClass) + { + + Entry entry = null; + + switch (value.GetType().Name) + { + case "String": + entry = new StringEntry(name, value as string); + break; + + case "Byte[]": + byte[] rawValue = value as byte[]; + // read raw content so it can be saved as a binary entry resource (byte[]) +#if NETFRAMEWORK + Bitmap bitmapImage = null; + bool imageJPeg = false; + bool imageGif = false; + bool imageBmp = false; + try + { + bitmapImage = Image.FromStream(new MemoryStream(rawValue)) as Bitmap; + imageJPeg = bitmapImage.RawFormat.Equals(ImageFormat.Jpeg); + imageGif = bitmapImage.RawFormat.Equals(ImageFormat.Gif); + imageBmp = bitmapImage.RawFormat.Equals(ImageFormat.Bmp); + } + catch + { + // Ignore error if not a known supported bitmap + } + if (imageJPeg || imageGif || imageBmp) + { + entry = new BitmapEntry(name, bitmapImage); + } +#else + if (rawValue != null && CrossPlatformBitmap.IsSupportedBitmap(rawValue)) + { + entry = new BitmapEntry(name, rawValue); + } +#endif + else if (rawValue != null) + { + // This code handles fonts which have a resource header starting with a Magic Number + // followed by a number of resources, e.g. each font letter etc... + entry = NanoResourcesEntry.TryCreateNanoResourcesEntry(name, rawValue); + + // If just a byte array, create a Binary Resource + if (entry == null) + { + // Create BinaryEntry + using (var ms = new MemoryStream(rawValue)) + { + entry = new BinaryEntry(name, rawValue); + } + } + } + break; + case "MemoryStream": + // Examples - .wav + // this is a binary resource + MemoryStream msOther = (MemoryStream)value; + byte[] memoryData = msOther.ToArray(); + entry = new BinaryEntry(name, memoryData); + break; + default: + break; + } + + if (entry == null) + { + throw new Exception($"Resource '{name}' has unsupported type '{value.GetType().FullName}'."); + } + + if (entry.Namespace.Length == 0) + { + entry.Namespace = defaultNamespace; + } + + if (entry.ClassName.Length == 0) + { + ResourceTypeDescription typeDescription = ResourceTypeDescriptionFromResourceType(entry.ResourceType); + entry.ClassName = typeDescription.defaultEnum; + + if (!string.IsNullOrEmpty(defaultDeclaringClass)) + { + entry.ClassName = $"{defaultDeclaringClass}+{entry.ClassName}"; + } + } + + return entry; + } + + private bool ParseId(string val, out short id) + { + val = val.Trim(); + bool fSuccess = false; + + if (val.StartsWith("0x", true, CultureInfo.InvariantCulture)) + { + ushort us; + + fSuccess = ushort.TryParse(val.Substring(2), NumberStyles.AllowHexSpecifier, null, out us); + + id = (short)us; + } + else + { + fSuccess = short.TryParse(val, out id); + } + + return fSuccess; + } + + public Entry(string name, object value) + { + Value = value; + Namespace = string.Empty; + ClassName = string.Empty; + field = string.Empty; + rawName = name; + + //parse name + + string[] tokens = name.Split(';'); + string idValue; + short idT; + + switch (tokens.Length) + { + case 1: + name = tokens[0]; + idValue = string.Empty; + break; + case 2: + name = tokens[0]; + idValue = tokens[1]; + break; + default: + throw new ArgumentException(); + } + + idValue = idValue.Trim(); + + if (idValue.Length > 0) + { + if (!ParseId(idValue, out idT)) + { + throw new ApplicationException(string.Format("Cannot parse id '{0}' from resource '{1}'", idValue, name)); + } + + Id = idT; + } + else + { + Id = GenerateIdFromResourceName(name); + } + + int iDotLast = name.LastIndexOf('.'); + + field = name; + + if (iDotLast >= 0) + { + field = name.Substring(iDotLast + 1); + + name = name.Substring(0, iDotLast); + + iDotLast = name.LastIndexOf('.'); + //iDotLast = name.LastIndexOfAny( new char[] { '.', '+' } ); + + ClassName = name.Trim(); + + if (iDotLast >= 0) + { + ClassName = name.Substring(iDotLast + 1).Trim(); + Namespace = name.Substring(0, iDotLast).Trim(); + } + } + } + + private string field; + private string rawName; + + public string Name + { + get { return string.Format("{0}.{1}.{2};0x{3}", Namespace, ClassName, field, Id.ToString("X4")); } + } + + public string RawName + { + get { return rawName; } + } + + public object Value { get; } + + public short Id { get; set; } + + public string Namespace { get; set; } + + public string ClassName { get; set; } + + public string Field + { + get { return field; } + set { field = value; } + } + + #region IComparable Members + + int IComparable.CompareTo(object obj) + { + Entry entry = obj as Entry; + + if (entry == null) + { + return -1; + } + + return Id.CompareTo(entry.Id); + } + + #endregion + + public virtual byte ResourceType + { + get + { + if (Value.GetType() == typeof(string)) return NanoResourceFile.ResourceHeader.RESOURCE_String; + + return NanoResourceFile.ResourceHeader.RESOURCE_Invalid; + } + } + + public virtual byte[] GenerateResourceData() + { + return null; + } + } + + private class StringEntry : Entry + { + public StringEntry(string name, string value) : base(name, value) + { + } + + private string StringValue + { + get { return Value as string; } + } + + public override byte ResourceType + { + get + { + return NanoResourceFile.ResourceHeader.RESOURCE_String; + } + } + + public override byte[] GenerateResourceData() + { + string val = StringValue + '\0'; + + byte[] data = Encoding.UTF8.GetBytes(val); + + return data; + } + } + + private class BitmapEntry : Entry + { +#if NETFRAMEWORK + public BitmapEntry(string name, System.Drawing.Bitmap value) : base(name, value) + { + } + + private System.Drawing.Bitmap BitmapValue + { + get { return Value as System.Drawing.Bitmap; } + } + + public override byte ResourceType + { + get + { + return NanoResourceFile.ResourceHeader.RESOURCE_Bitmap; + } + } + + + private void Adjust1bppOrientation(byte[] buf) + { + //CLR_GFX_Bitmap::AdjustBitOrientation + //The nanoCLR treats 1bpp bitmaps reversed from Windows + //And most likely every other 1bpp format as well + byte[] reverseTable = new byte[] + { + 0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, + 0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0, + 0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, + 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8, + 0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4, + 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4, + 0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, + 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC, + 0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2, + 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2, + 0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA, + 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA, + 0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, + 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6, + 0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, + 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE, + 0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1, + 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1, + 0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, + 0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9, + 0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, + 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5, + 0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED, + 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD, + 0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, + 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3, + 0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, + 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB, + 0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7, + 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, + 0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF, + 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F,0xFF, + }; + + for (int i = buf.Length - 1; i >= 0; i--) + { + buf[i] = reverseTable[buf[i]]; + } + } + + private void Compress1bpp(NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription, ref byte[] buf) + { + MemoryStream ms = new MemoryStream(buf.Length); + + //adapted from CLR_GFX_Bitmap::Compress + //CLR_RT_Buffer buffer; + int count = 0; + bool fSetSav = false; + bool fSet = false; + bool fFirst = true; + bool fRun = true; + byte data = 0; + bool fEmit = false; + int widthInWords = (int)((bitmapDescription.m_width + 31) / 32); + int iByte; + byte iPixelMask; + + iByte = 0; + for (int y = 0; y < bitmapDescription.m_height; y++) + { + iPixelMask = 0x1; + iByte = y * (widthInWords * 4); + + for (int x = 0; x < bitmapDescription.m_width; x++) + { + fSetSav = fSet; + + fSet = (buf[iByte] & iPixelMask) != 0; + if (fFirst) + { + fFirst = false; + } + else + { + if (fRun) + { + fRun = (fSetSav == fSet); + + if ((count == 0x3f + NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunOffset) || + (!fRun && count >= NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunOffset)) + { + data = NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRun; + data |= (fSetSav ? NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunSet : (byte)0x0); + data |= (byte)(count - NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunOffset); + fEmit = true; + } + } + + if (!fRun && count == NanoResourceFile.CLR_GFX_BitmapDescription.c_UncompressedRunLength) + { + fEmit = true; + } + + if (fEmit) + { + ms.WriteByte(data); + + data = 0; + count = 0; + fEmit = false; + fRun = true; + } + } + + data |= (byte)((0x1 << count) & (fSet ? 0xff : 0x0)); + + iPixelMask <<= 1; + if (iPixelMask == 0) + { + iPixelMask = 0x1; + iByte++; + } + + count++; + } + } + + if (fRun && count >= NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunOffset) + { + data = NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRun; + data |= (fSetSav ? NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunSet : (byte)0x0); + data |= (byte)(count - NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunOffset); + } + + ms.WriteByte(data); + + if (ms.Length < buf.Length) + { + ms.Capacity = (int)ms.Length; + buf = ms.GetBuffer(); + + bitmapDescription.m_flags |= NanoResourceFile.CLR_GFX_BitmapDescription.c_Compressed; + } + } + private byte[] GetBitmapDataBmp(Bitmap bitmap, out NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription) + { + + + try + { + byte bitsPerPixel = 16; + Bitmap clone = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format16bppRgb565); + using (Graphics gr = Graphics.FromImage(clone)) + { + gr.DrawImageUnscaled(bitmap, 0, 0); + } + + Rectangle rect = new Rectangle(0, 0, clone.Width, clone.Height); + BitmapData bitmapData = clone.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format16bppRgb565); + + byte[] data = new byte[clone.Width * clone.Height * 2]; + + System.Runtime.InteropServices.Marshal.Copy(bitmapData.Scan0, data, 0, data.Length); + clone.UnlockBits(bitmapData); + + bitmapDescription = new NanoResourceFile.CLR_GFX_BitmapDescription( + (ushort)bitmap.Width, (ushort)bitmap.Height, 0, bitsPerPixel, NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeBitmap); + + return data; + } + catch + { + throw new NotSupportedException($"PixelFormat ({bitmap.PixelFormat.ToString()}) could not be converted to Format16bppRgb565."); + }; + } + + private byte[] GetBitmapDataRaw(Bitmap bitmap, out NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription, byte type) + { + bitmapDescription = new NanoResourceFile.CLR_GFX_BitmapDescription((ushort)bitmap.Width, (ushort)bitmap.Height, 0, 1, type); + + MemoryStream stream = new MemoryStream(); + + // need to copy the Bitmap in order to access it, otherwise it will throw an exception + using (Bitmap bmpCopy = new Bitmap(bitmap)) + { + bmpCopy.Save(stream, bitmap.RawFormat); + } + + stream.Capacity = (int)stream.Length; + return stream.GetBuffer(); + } + + private byte[] GetBitmapDataJpeg(Bitmap bitmap, out NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription) + { + return GetBitmapDataRaw(bitmap, out bitmapDescription, NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeJpeg); + } + + private byte[] GetBitmapDataGif(Bitmap bitmap, out NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription) + { + return GetBitmapDataRaw(bitmap, out bitmapDescription, NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeGif); + } + + private byte[] GetBitmapData(Bitmap bitmap, out NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription) + { + byte[] data = null; + + if (bitmap.Width > 0xFFFF || bitmap.Height > 0xFFFF) + { + throw new ArgumentException("bitmap dimensions out of range"); + } + + if (bitmap.RawFormat.Equals(ImageFormat.Jpeg)) + { + data = GetBitmapDataJpeg(bitmap, out bitmapDescription); + } + else if (bitmap.RawFormat.Equals(ImageFormat.Gif)) + { + data = GetBitmapDataGif(bitmap, out bitmapDescription); + } + else if (bitmap.RawFormat.Equals(ImageFormat.Bmp)) + { + data = GetBitmapDataBmp(bitmap, out bitmapDescription); + } + else + { + throw new NotSupportedException(string.Format("Bitmap imageFormat not supported '{0}'", bitmap.RawFormat.Guid.ToString())); + } + + return data; + } + + public override byte[] GenerateResourceData() + { + Bitmap bitmap = BitmapValue; + + NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription; + + byte[] data = GetBitmapData(bitmap, out bitmapDescription); + + MemoryStream stream = new MemoryStream(); + BinaryWriter writer = new BinaryWriter(stream); + + bitmapDescription.Serialize(writer); + writer.Write(data); + + stream.Capacity = (int)stream.Length; + return stream.GetBuffer(); + } + + /* + public override byte[] GenerateResourceData() + { + byte[] data = null; + + ushort flags = 0; + byte type = 0; + byte bitsPerPixel = 24; + BitmapData bitmapData = null; + Bitmap bitmap = this.BitmapValue; + PixelFormat formatDst = bitmap.PixelFormat; + ushort width = (ushort)bitmap.Width; + ushort height = (ushort)bitmap.Height; + NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription; + + Rectangle rect = new Rectangle( 0, 0, this.BitmapValue.Width, this.BitmapValue.Height ); + + if(bitmap.RawFormat.Equals(ImageFormat.Jpeg)) + { + type = NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeJpeg; + } + else if(bitmap.RawFormat.Equals(ImageFormat.Gif)) + { + type = NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeGif; + } + else if(bitmap.RawFormat.Equals(ImageFormat.Bmp)) + { + type = NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeBitmap; + + //issue warning for formats that we lose information? + //other formats that we need to support?? + + switch(bitmap.PixelFormat) + { + case PixelFormat.Format1bppIndexed: + bitsPerPixel = 1; + formatDst = PixelFormat.Format1bppIndexed; + break; + case PixelFormat.Format24bppRgb: + case PixelFormat.Format32bppRgb: + case PixelFormat.Format48bppRgb: + //currently don't support more than 16bp...fall through.. + case PixelFormat.Format16bppRgb555: + case PixelFormat.Format16bppRgb565: + bitsPerPixel = 16; + formatDst = PixelFormat.Format16bppRgb565; + break; + default: + throw new NotSupportedException( string.Format( "PixelFormat of '{0}' resource not supported", this.Name ) ); + } + + //turn bitmap data into a form we can use. + + if(formatDst != bitmap.PixelFormat) + { + bitmap = bitmap.Clone( rect, formatDst ); + } + } + else + { + throw new NotSupportedException( string.Format("Bitmap imageFormat not supported '{0}'", bitmap.RawFormat.Guid.ToString()) ); + } + + try + { + bitmapData = bitmap.LockBits( rect, ImageLockMode.ReadOnly, formatDst ); + + IntPtr p = bitmapData.Scan0; + data = new byte[bitmapData.Stride * height]; + + System.Runtime.InteropServices.Marshal.Copy( bitmapData.Scan0, data, 0, data.Length ); + + if(bitsPerPixel == 1) + { + //special case for 1pp with index 0 equals white???!!!??? + if(bitmap.Palette.Entries[0].GetBrightness() < 0.5) + { + for(int i = 0; i < data.Length; i++) + { + data[i] = (byte)~data[i]; + } + } + + //special case for 1pp need to flip orientation?? + //for some stupid reason, 1bpp is flipped compared to windows!! + Adjust1bppOrientation( data ); + } + } + finally + { + if(bitmapData != null) + { + bitmap.UnlockBits( bitmapData ); + } + } + + NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription = new NanoResourceFile.CLR_GFX_BitmapDescription( width, height, flags, bitsPerPixel, type ); + + if(bitsPerPixel == 1 && type == NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeBitmap) + { + //test compression; + Compress1bpp( bitmapDescription, ref data ); + } + + MemoryStream stream = new MemoryStream(); + BinaryWriter writer = new BinaryWriter( stream ); + + bitmapDescription.Serialize( writer ); + writer.Write( data ); + + byte[] buf = new byte[stream.Length]; + + Array.Copy( stream.GetBuffer(), buf, stream.Length ); + + return buf; + } + */ +#else + private byte[] RawValue + { + get { return Value as byte[]; } + } + + public BitmapEntry(string name, byte[] value) : base(name, value) + { + } + + public override byte ResourceType + { + get + { + return NanoResourceFile.ResourceHeader.RESOURCE_Bitmap; + } + } + + public override byte[] GenerateResourceData() + { + return CrossPlatformBitmap.GenerateBitmapResourceData(RawValue); + } +#endif + } + + private class NanoResourcesEntry : Entry + { + NanoResourceFile.ResourceHeader resource; + + public NanoResourcesEntry(string name, byte[] value) : base(name, value) + { + } + + public static NanoResourcesEntry TryCreateNanoResourcesEntry(string name, byte[] value) + { + NanoResourcesEntry entry = null; + + try + { + MemoryStream stream = new MemoryStream(value); + + BinaryReader reader = new BinaryReader(stream); + uint magicNumber = reader.ReadUInt32(); + + stream.Position = 0; + + if (magicNumber == NanoResourceFile.Header.MAGIC_NUMBER) + { + NanoResourceFile file = new NanoResourceFile(); + + file.Deserialize(reader); + + if (file.resources.Length == 1) + { + NanoResourceFile.Resource resource = file.resources[0]; + + entry = new NanoResourcesEntry(name, resource.data); + entry.resource = resource.header; + } + } + } + catch + { + } + + return entry; + } + + private byte[] RawValue + { + get { return Value as byte[]; } + } + + public override byte ResourceType + { + get + { + return resource.kind; + } + } + + public override byte[] GenerateResourceData() + { + return RawValue; + } + + } + + private class BinaryEntry : Entry + { + public BinaryEntry(string name, byte[] value) : base(name, value) + { + + } + + private byte[] BinaryValue + { + get { return Value as byte[]; } + } + + public override byte ResourceType + { + get + { + return NanoResourceFile.ResourceHeader.RESOURCE_Binary; + } + } + + public override byte[] GenerateResourceData() + { + return BinaryValue; + } + } + + #endregion // Code from ResGen.EXE + + internal class NanoResourceFile + { + /* + .nanoresources file format. The Header is shared with the Metadataprocessor. Everything else + is shared with the nanoCLR> + + ------- + Header + ------- + + uint MagicNumber = 0xf995b0a8; + uint Version; + uint SizeOfHeader; + uint SizeOfResourceHeader; //size of all CLR_RECORD_RESOURCE structures to follow + uint NumberOfResources; //number of resources + + --------------------- + Resource Headers + --------------------- + + Starting at Header.SizeOfHeader in the stream, + Header.NumberOfResources resourceHeader structures + + + + */ + + public Header header; + public Resource[] resources; + + public class Resource + { + public ResourceHeader header; + public byte[] data; + + public Resource(ResourceHeader header, byte[] data) + { + this.header = header; + this.data = data; + } + } + + public NanoResourceFile() + { + resources = new Resource[0]; + } + + public NanoResourceFile(Header header) : this() + { + this.header = header; + } + + public void AddResource(Resource resource) + { + int cResource = resources.Length; + + Resource[] resourcesNew = new Resource[cResource + 1]; + resources.CopyTo(resourcesNew, 0); + resourcesNew[cResource] = resource; + resources = resourcesNew; + } + + public void Serialize(BinaryWriter writer) + { + header.Serialize(writer); + + for (int iResource = 0; iResource < resources.Length; iResource++) + { + Resource resource = resources[iResource]; + + resource.header.Serialize(writer); + writer.Write(resource.data); + } + } + + public void Deserialize(BinaryReader reader) + { + header = new Header(); + header.Deserialize(reader); + + if (header.NumberOfResources == 0) + { + throw new SerializationException("No resources found"); + } + + resources = new Resource[header.NumberOfResources]; + + for (int iResource = 0; iResource < resources.Length; iResource++) + { + Resource resource = new Resource(new ResourceHeader(), new byte[0]); + + resources[iResource] = resource; + resource.header.Deserialize(reader); + resource.data = reader.ReadBytes((int)resource.header.size); + } + } + + #region Records + + public class Header + { + public const uint VERSION = 2; + public const uint MAGIC_NUMBER = 0xf995b0a8; + public const uint SIZE_FILE_HEADER = 5 * 4; + public const uint SIZE_RESOURCE_HEADER = 2 * 4; + + public uint MagicNumber; + public uint Version; + public uint SizeOfHeader; + public uint SizeOfResourceHeader; + public uint NumberOfResources; + + public Header(uint numResources) + { + MagicNumber = MAGIC_NUMBER; + Version = VERSION; + SizeOfHeader = SIZE_FILE_HEADER; + SizeOfResourceHeader = SIZE_RESOURCE_HEADER; + NumberOfResources = numResources; + } + + public Header() + { + } + + public void Serialize(BinaryWriter writer) + { + writer.Write(MagicNumber); + writer.Write(Version); + writer.Write(SizeOfHeader); + writer.Write(SizeOfResourceHeader); + writer.Write(NumberOfResources); + } + + public void Deserialize(BinaryReader reader) + { + MagicNumber = reader.ReadUInt32(); + Version = reader.ReadUInt32(); + SizeOfHeader = reader.ReadUInt32(); + SizeOfResourceHeader = reader.ReadUInt32(); + NumberOfResources = reader.ReadUInt32(); + + reader.BaseStream.Position = SizeOfHeader; + + if (MagicNumber != MAGIC_NUMBER || + SizeOfHeader < SIZE_FILE_HEADER || + SizeOfResourceHeader < SIZE_RESOURCE_HEADER + ) + { + throw new SerializationException(); + } + else if (Version != VERSION) + { + throw new SerializationException(string.Format("Incompatible version (version {0}) found, expecting version {1}.", Version, VERSION)); + } + } + + public uint OffsetOfResourceData + { + get + { + return SizeOfHeader + SizeOfResourceHeader * NumberOfResources; + } + } + } + + public class ResourceHeader + { + public const byte RESOURCE_Invalid = 0x00; + public const byte RESOURCE_Bitmap = 0x01; + public const byte RESOURCE_Font = 0x02; + public const byte RESOURCE_String = 0x03; + public const byte RESOURCE_Binary = 0x04; + public const byte RESOURCE_Max = RESOURCE_Binary; + + public short id; + public byte kind; + public byte pad; + public uint size; + + public ResourceHeader(short id, byte kind, uint size) + { + this.id = id; + this.kind = kind; + pad = 0; + this.size = size; + } + + public ResourceHeader() + { + } + + public void Serialize(BinaryWriter writer) + { + writer.Write(id); + writer.Write(kind); + writer.Write(pad); + writer.Write(size); + } + + public void Deserialize(BinaryReader reader) + { + id = reader.ReadInt16(); + kind = reader.ReadByte(); + pad = reader.ReadByte(); + size = reader.ReadUInt32(); + } + } + + public class CLR_GFX_BitmapDescription + { + public const ushort c_ReadOnly = 0x0001; + public const ushort c_Compressed = 0x0002; + + public const ushort c_Rotation0 = 0x0000; + public const ushort c_Rotation90 = 0x0004; + public const ushort c_Rotation180 = 0x0008; + public const ushort c_Rotation270 = 0x000b; + public const ushort c_RotationMask = 0x000b; + + public const byte c_CompressedRun = 0x80; + public const byte c_CompressedRunSet = 0x40; + public const byte c_CompressedRunLengthMask = 0x3f; + public const byte c_UncompressedRunLength = 7; + public const byte c_CompressedRunOffset = c_UncompressedRunLength + 1; + + // Note that these type definitions has to match the ones defined in + // Bitmap.BitmapImageType enum defined in Graphics.cs + public const byte c_TypeBitmap = 0; + public const byte c_TypeGif = 1; + public const byte c_TypeJpeg = 2; + + // !!!!WARNING!!!! + // These fields should correspond to CLR_GFX_BitmapDescription in NanoCLR_Graphics.h + // and should be 4-byte aligned in size. When these fields are changed, the version number + // of the nanoresource file should be incremented, the nanofnts should be updated (buildhelper -convertfont ...) + // and the MMP should also be updated as well. (Consult rwolff before touching this.) + public uint m_width; + public uint m_height; + public ushort m_flags; + public byte m_bitsPerPixel; + public byte m_type; + + public CLR_GFX_BitmapDescription(ushort width, ushort height, ushort flags, byte bitsPerPixel, byte type) + { + m_width = width; + m_height = height; + m_flags = flags; + m_bitsPerPixel = bitsPerPixel; + m_type = type; + } + + public CLR_GFX_BitmapDescription() + { + } + + public void Serialize(BinaryWriter writer) + { + writer.Write(m_width); + writer.Write(m_height); + writer.Write(m_flags); + writer.Write(m_bitsPerPixel); + writer.Write(m_type); + } + + public void Deserialize(BinaryReader reader) + { + m_width = reader.ReadUInt16(); + m_height = reader.ReadUInt16(); + m_flags = reader.ReadUInt16(); + m_bitsPerPixel = reader.ReadByte(); + m_type = reader.ReadByte(); + } + } + #endregion + } + + internal class NanoResourceWriter : IResourceWriter + { + string fileName; + ArrayList resources; + + public NanoResourceWriter(string fileName) + { + this.fileName = fileName; + resources = new ArrayList(); + } + + public void AddResource(Entry entry) + { + resources.Add(entry); + } + + private void Add(string name, object value) + { + Entry entry = Entry.CreateEntry(name, value, string.Empty, string.Empty); + resources.Add(entry); + } + + #region IResourceWriter Members + + void IResourceWriter.AddResource(string name, byte[] value) + { + Add(name, value); + } + + void IResourceWriter.AddResource(string name, object value) + { + Add(name, value); + } + + void IResourceWriter.AddResource(string name, string value) + { + Add(name, value); + } + + void IResourceWriter.Close() + { + ((IDisposable)this).Dispose(); + } + + void IResourceWriter.Generate() + { + //PrepareToGenerate(); + ProcessResourceFiles.EnsureResourcesIds(resources); + + NanoResourceFile.Header header = new NanoResourceFile.Header((uint)resources.Count); + NanoResourceFile file = new NanoResourceFile(header); + + for (int iResource = 0; iResource < resources.Count; iResource++) + { + Entry entry = (Entry)resources[iResource]; + + byte[] data = entry.GenerateResourceData(); + NanoResourceFile.ResourceHeader resource = new NanoResourceFile.ResourceHeader(entry.Id, entry.ResourceType, (uint)data.Length); + + file.AddResource(new NanoResourceFile.Resource(resource, data)); + } + + using (FileStream fileStream = File.Open(fileName, FileMode.OpenOrCreate)) + { + BinaryWriter writer = new BinaryWriter(fileStream); + file.Serialize(writer); + fileStream.Flush(); + } + } + + #endregion + + #region IDisposable Members + + void IDisposable.Dispose() + { + } + + #endregion + } + + public class NanoResourceReader : IResourceReader + { + + #region IResourceReader Members + + void IResourceReader.Close() + { + throw new NotImplementedException(); + } + + IDictionaryEnumerator IResourceReader.GetEnumerator() + { + throw new NotImplementedException(); + } + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + + #endregion + + #region IDisposable Members + + void IDisposable.Dispose() + { + throw new NotImplementedException(); + } + + #endregion + } + } + +} diff --git a/nanoFramework.Tools.BuildTasks/ResolveRuntimeDependenciesTask.cs b/nanoFramework.Tools.BuildTasks/ResolveRuntimeDependenciesTask.cs new file mode 100644 index 0000000..624f78d --- /dev/null +++ b/nanoFramework.Tools.BuildTasks/ResolveRuntimeDependenciesTask.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System; +using System.ComponentModel; +using System.Linq; + +namespace nanoFramework.Tools +{ + [Description("ResolveRuntimeDependenciesTaskEntry")] + public class ResolveRuntimeDependenciesTask : Task + { + #region public properties for the task + + public string Assembly { get; set; } + + public string StartProgram { get; set; } + + public ITaskItem[] AssemblyReferences { get; set; } + + public ITaskItem[] StartProgramReferences { get; set; } + + #endregion + + + public override bool Execute() + { + // report to VS output window what step the build is + Log.LogMessage(MessageImportance.Normal, "Resolving Runtime Dependencies for nanoFramework assembly..."); + + try + { + object host = HostObject; + + if (host != null) + { + Type typ = host.GetType(); + + typ.GetProperty("Assembly").SetValue(host, Assembly, null); + typ.GetProperty("StartProgram").SetValue(host, StartProgram, null); + typ.GetProperty("AssemblyReferences").SetValue(host, GetFullPathFromItems(AssemblyReferences), null); + typ.GetProperty("StartProgramReferences").SetValue(host, GetFullPathFromItems(StartProgramReferences), null); + } + } + catch (Exception ex) + { + Log.LogError(".NET nanoFramework ResolveRuntimeDependenciesTask error: " + ex.Message); + } + + // if we've logged any errors that's because there were errors (WOW!) + return !Log.HasLoggedErrors; + } + + protected string[] GetFullPathFromItems(ITaskItem[] items) + { + return items?.Select((item) => { + return item.GetMetadata("FullPath"); + }).ToArray(); + } + } +} diff --git a/nanoFramework.Tools.BuildTasks/TasksConstants.cs b/nanoFramework.Tools.BuildTasks/TasksConstants.cs new file mode 100644 index 0000000..3dc3415 --- /dev/null +++ b/nanoFramework.Tools.BuildTasks/TasksConstants.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +namespace nanoFramework.Tools +{ + internal class TasksConstants + { + public const string BuildTaskDebugVar = "NFBUILD_TASKS_DEBUG"; + } +} diff --git a/nanoFramework.Tools.BuildTasks/Utilities/DebuggerHelper.cs b/nanoFramework.Tools.BuildTasks/Utilities/DebuggerHelper.cs new file mode 100644 index 0000000..6246b3f --- /dev/null +++ b/nanoFramework.Tools.BuildTasks/Utilities/DebuggerHelper.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) .NET Foundation and Contributors +// Portions Copyright (c) Microsoft Corporation. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +using System; +using System.Diagnostics; +using System.Threading; + +namespace nanoFramework.Tools.Utilities +{ + public static class DebuggerHelper + { + public static void WaitForDebuggerIfEnabled(string varName, int timeoutSeconds = 30) + { + + // this wait should be only available on debug build + // to prevent unwanted wait on VS in machines where the variable is present +#if DEBUG + TimeSpan waitForDebugToAttach = TimeSpan.FromSeconds(timeoutSeconds); + + var debugEnabled = Environment.GetEnvironmentVariable(varName, EnvironmentVariableTarget.User); + + if (!string.IsNullOrEmpty(debugEnabled) && debugEnabled.Equals("1", StringComparison.Ordinal)) + { + Console.WriteLine($".NET nanoFramework Metadata Processor msbuild instrumentation task debugging is enabled. Waiting {timeoutSeconds} seconds for debugger attachment..."); + + var currentProcessId = Process.GetCurrentProcess().Id; + var currentProcessName = Process.GetProcessById(currentProcessId).ProcessName; + Console.WriteLine( + string.Format("Process Id: {0}, Name: {1}", currentProcessId, currentProcessName) + ); + + // wait N seconds for debugger to attach + while (!Debugger.IsAttached && waitForDebugToAttach.TotalSeconds > 0) + { + Thread.Sleep(1000); + waitForDebugToAttach -= TimeSpan.FromSeconds(1); + } + + Debugger.Break(); + } +#endif + } + } +} diff --git a/nanoFramework.Tools.BuildTasks/nanoFramework.Tools.BuildTasks.csproj b/nanoFramework.Tools.BuildTasks/nanoFramework.Tools.BuildTasks.csproj index 09305e9..4d1d658 100644 --- a/nanoFramework.Tools.BuildTasks/nanoFramework.Tools.BuildTasks.csproj +++ b/nanoFramework.Tools.BuildTasks/nanoFramework.Tools.BuildTasks.csproj @@ -8,6 +8,12 @@ false + + + <_Parameter1>nanoFramework.Tools.BuildTasks.Tests + + + compile; build; native; contentfiles; analyzers; buildtransitive @@ -17,37 +23,33 @@ - + - - - - - - - - - + + + - - - - + + true + diff --git a/test/BuildTasks.Tests/BuildTasks.Tests.csproj b/test/BuildTasks.Tests/BuildTasks.Tests.csproj new file mode 100644 index 0000000..877f9d1 --- /dev/null +++ b/test/BuildTasks.Tests/BuildTasks.Tests.csproj @@ -0,0 +1,38 @@ + + + + net8.0;net472 + nanoFramework.Tools.BuildTasks.Tests + nanoFramework.Tools.BuildTasks.Tests + false + true + latest + enable + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/BuildTasks.Tests/Infrastructure/TestBuildEngine.cs b/test/BuildTasks.Tests/Infrastructure/TestBuildEngine.cs new file mode 100644 index 0000000..598b180 --- /dev/null +++ b/test/BuildTasks.Tests/Infrastructure/TestBuildEngine.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Build.Framework; + +namespace nanoFramework.Tools.BuildTasks.Tests.Infrastructure +{ + /// + /// Minimal that captures logged events so tests can + /// assert on warnings and errors without a real MSBuild host. + /// + internal sealed class TestBuildEngine : IBuildEngine + { + public List Warnings { get; } = new List(); + public List Errors { get; } = new List(); + public List Messages { get; } = new List(); + + public void LogWarningEvent(BuildWarningEventArgs e) => Warnings.Add(e.Message ?? string.Empty); + public void LogErrorEvent(BuildErrorEventArgs e) => Errors.Add(e.Message ?? string.Empty); + public void LogMessageEvent(BuildMessageEventArgs e) => Messages.Add(e.Message ?? string.Empty); + public void LogCustomEvent(CustomBuildEventArgs e) { } + + public bool BuildProjectFile(string projectFileName, string[] targetNames, + IDictionary globalProperties, IDictionary targetOutputs) => + throw new NotImplementedException(); + + public bool ContinueOnError => false; + public int LineNumberOfTaskNode => 0; + public int ColumnNumberOfTaskNode => 0; + public string ProjectFileOfTaskNode => string.Empty; + } + + /// + /// Minimal implementation that wires up a . + /// Used to construct a without a real MSBuild host. + /// + internal sealed class TestTask : ITask + { + public IBuildEngine BuildEngine { get; set; } = null!; + public ITaskHost? HostObject { get; set; } + public bool Execute() => true; + } + +} diff --git a/test/BuildTasks.Tests/NanoResxResourceReaderTests.cs b/test/BuildTasks.Tests/NanoResxResourceReaderTests.cs new file mode 100644 index 0000000..0da7211 --- /dev/null +++ b/test/BuildTasks.Tests/NanoResxResourceReaderTests.cs @@ -0,0 +1,255 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +// NanoResxResourceReader is only compiled on non-Framework targets (see +// ProcessResourceFiles.CrossPlatform.cs). The tests here mirror that guard. +#if !NETFRAMEWORK + +using System; +using System.IO; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using nanoFramework.Tools; + +namespace nanoFramework.Tools.BuildTasks.Tests +{ + /// + /// Unit tests for , the cross-platform + /// (net8.0+) .resx parser used in place of + /// System.Resources.ResXResourceReader (WinForms / net472 only). + /// + [TestClass] + public class NanoResxResourceReaderTests + { + // ─── helpers ─────────────────────────────────────────────────────────────── + + private const string ResxHeader = @" + + text/microsoft-resx + 2.0"; + + private const string ResxFooter = "\n"; + + private static NanoResxResourceReader ParseResx(string resxBody, string? basePath = null) + { + string xml = ResxHeader + resxBody + ResxFooter; + using var sr = new StringReader(xml); + return new NanoResxResourceReader(sr, basePath ?? Path.GetTempPath()); + } + + private static System.Collections.Hashtable ToHashtable(NanoResxResourceReader reader) + { + var ht = new System.Collections.Hashtable(); + var en = reader.GetEnumerator(); + while (en.MoveNext()) + ht[en.Key] = en.Value; + return ht; + } + + // ─── inline string resources ─────────────────────────────────────────────── + + [TestMethod] + public void Parse_InlineString_ReturnsStringValue() + { + var reader = ParseResx(@" + Hello nanoFramework"); + + var resources = ToHashtable(reader); + + Assert.AreEqual(1, resources.Count); + Assert.IsInstanceOfType(resources["Greeting"], typeof(string)); + Assert.AreEqual("Hello nanoFramework", (string)resources["Greeting"]!); + } + + [TestMethod] + public void Parse_MultipleInlineStrings_AllPresent() + { + var reader = ParseResx(@" + alpha + beta + gamma"); + + var resources = ToHashtable(reader); + + Assert.AreEqual(3, resources.Count); + Assert.AreEqual("alpha", resources["A"]); + Assert.AreEqual("beta", resources["B"]); + Assert.AreEqual("gamma", resources["C"]); + } + + [TestMethod] + public void Parse_EmptyStringValue_ReturnsEmptyString() + { + var reader = ParseResx(@" + "); + + var resources = ToHashtable(reader); + + Assert.AreEqual(1, resources.Count); + Assert.AreEqual(string.Empty, resources["Empty"]); + } + + [TestMethod] + public void Parse_MissingNameAttribute_EntryIsSkipped() + { + // An entry without a 'name' attribute should be silently ignored. + var reader = ParseResx(@" + no name + ok"); + + var resources = ToHashtable(reader); + + Assert.AreEqual(1, resources.Count, "nameless entry should be skipped"); + Assert.IsTrue(resources.ContainsKey("Valid")); + } + + // ─── inline base64 byte arrays ───────────────────────────────────────────── + + [TestMethod] + public void Parse_InlineBase64ByteArray_ReturnsByteArray() + { + byte[] expected = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + string base64 = Convert.ToBase64String(expected); + + var reader = ParseResx($@" + + {base64} + "); + + var resources = ToHashtable(reader); + + Assert.AreEqual(1, resources.Count); + Assert.IsInstanceOfType(resources["Blob"], typeof(byte[])); + CollectionAssert.AreEqual(expected, (byte[])resources["Blob"]!); + } + + [TestMethod] + public void Parse_UnsupportedMimetype_ThrowsNotSupportedException() + { + Assert.ThrowsException(() => + { + var reader = ParseResx(@" + + AQID + "); + // the exception is thrown during construction (inside Parse) + }); + } + + // ─── ResXFileRef (file references) ──────────────────────────────────────── + + [TestMethod] + public void Parse_FileRef_ByteArray_ReturnsByteArray() + { + string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(dir); + try + { + byte[] expected = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }; + File.WriteAllBytes(Path.Combine(dir, "blob.bin"), expected); + + var reader = ParseResx(@" + + blob.bin;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ", basePath: dir); + + var resources = ToHashtable(reader); + + Assert.AreEqual(1, resources.Count); + Assert.IsInstanceOfType(resources["Blob"], typeof(byte[])); + CollectionAssert.AreEqual(expected, (byte[])resources["Blob"]!); + } + finally + { + Directory.Delete(dir, true); + } + } + + [TestMethod] + public void Parse_FileRef_String_ReturnsStringContent() + { + string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(dir); + try + { + const string content = "Hello from file"; + File.WriteAllText(Path.Combine(dir, "text.txt"), content, Encoding.UTF8); + + var reader = ParseResx(@" + + text.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ", basePath: dir); + + var resources = ToHashtable(reader); + + Assert.AreEqual(1, resources.Count); + Assert.IsInstanceOfType(resources["FileText"], typeof(string)); + Assert.AreEqual(content, (string)resources["FileText"]!); + } + finally + { + Directory.Delete(dir, true); + } + } + + [TestMethod] + public void Parse_FileRef_AbsolutePath_Resolved() + { + string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(dir); + try + { + byte[] expected = new byte[] { 0xCA, 0xFE }; + string absPath = Path.Combine(dir, "abs.bin"); + File.WriteAllBytes(absPath, expected); + + // Use an absolute path in the file ref value + string absPathEscaped = absPath.Replace("\\", "/"); + var reader = ParseResx($@" + + {absPath};System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ", basePath: null); + + var resources = ToHashtable(reader); + + Assert.AreEqual(1, resources.Count); + CollectionAssert.AreEqual(expected, (byte[])resources["Abs"]!); + } + finally + { + Directory.Delete(dir, true); + } + } + + [TestMethod] + public void Parse_UnsupportedType_ThrowsNotSupportedException() + { + // A resource with an explicit non-file-ref, non-string type is unsupported. + Assert.ThrowsException(() => + { + var reader = ParseResx(@" + + AQID + "); + }); + } + + [TestMethod] + public void Parse_ResxHeaderEntries_Ignored() + { + // nodes must not appear in the resource enumeration. + var reader = ParseResx(@" + hello"); + + var resources = ToHashtable(reader); + + // Only the one entry — resheader nodes not counted + Assert.AreEqual(1, resources.Count); + Assert.IsTrue(resources.ContainsKey("MyString")); + } + } +} + +#endif diff --git a/test/BuildTasks.Tests/ProcessResourceFilesTests.cs b/test/BuildTasks.Tests/ProcessResourceFilesTests.cs new file mode 100644 index 0000000..ae5f77d --- /dev/null +++ b/test/BuildTasks.Tests/ProcessResourceFilesTests.cs @@ -0,0 +1,320 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +// ProcessResourceFiles has a field of type TaskLoggingHelper +// (Microsoft.Build.Utilities.Core). On net8.0 that NuGet package ships only a +// reference assembly (no runtime lib), so instantiating ProcessResourceFiles fails +// at test-run time. The cross-platform components it exercises on net8.0 +// (NanoResxResourceReader, CrossPlatformBitmap) are covered directly by the other +// test files. These integration tests therefore run on net472 only. +#if NETFRAMEWORK + +using System; +using System.IO; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using nanoFramework.Tools; + +namespace nanoFramework.Tools.BuildTasks.Tests +{ + /// + /// End-to-end tests for : creates real .resx + /// files on disk, runs the resource processor, and validates the produced + /// .nanoresources binary output. + /// + /// These tests use the public ReadResources / WriteResources API + /// rather than Run() so that the test project has no runtime dependency on + /// Microsoft.Build.Utilities.Core (whose assembly is not on the default + /// probe path when running dotnet test). Both TFMs (net8.0 and net472) are + /// exercised. + /// + [TestClass] + public class ProcessResourceFilesTests + { + // ─── minimal .resx template ──────────────────────────────────────────────── + + private const string ResxHeader = @" + + text/microsoft-resx + 2.0 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"; + + private const string ResxFooter = "\n"; + + // ─── helpers ─────────────────────────────────────────────────────────────── + + /// + /// Runs + + /// on a temp .resx file and + /// returns the path to the produced .nanoresources file. + /// + private static string RunOnResx(string dir, string resxBody, bool useSourcePath = true) + { + string resxPath = Path.Combine(dir, "Resources.resx"); + string outPath = Path.Combine(dir, "Resources.nanoresources"); + File.WriteAllText(resxPath, ResxHeader + resxBody + ResxFooter, Encoding.UTF8); + + var prf = new ProcessResourceFiles(); + // StronglyTypedNamespace must be non-empty so Entry.CreateEntry works. + prf.StronglyTypedNamespace = "Test"; + prf.StronglyTypedClassName = "Resources"; + + prf.ReadResources(resxPath, useSourcePath); + prf.WriteResources(outPath); + + return outPath; + } + + /// Deserializes a .nanoresources file. + private static ProcessResourceFiles.NanoResourceFile ReadNanoresources(string path) + { + var nrf = new ProcessResourceFiles.NanoResourceFile(); + using var br = new BinaryReader(File.OpenRead(path)); + nrf.Deserialize(br); + return nrf; + } + + /// + /// Creates a minimal valid 24bpp 1×1 BMP with the given RGB pixel. + /// BMP pixels are stored as BGR, rows are bottom-up and + /// padded to a 4-byte boundary. + /// + private static byte[] MakeOnePxBmp(byte r, byte g, byte b) + { + // 14-byte file header + 40-byte BITMAPINFOHEADER + 4-byte pixel row + var bmp = new byte[58]; + + // File header + bmp[0] = 0x42; bmp[1] = 0x4D; + Buffer.BlockCopy(BitConverter.GetBytes(58u), 0, bmp, 2, 4); // file size + Buffer.BlockCopy(BitConverter.GetBytes(54u), 0, bmp, 10, 4); // pixel offset + + // BITMAPINFOHEADER + Buffer.BlockCopy(BitConverter.GetBytes(40u), 0, bmp, 14, 4); // header size + Buffer.BlockCopy(BitConverter.GetBytes(1u), 0, bmp, 18, 4); // width = 1 + Buffer.BlockCopy(BitConverter.GetBytes(1u), 0, bmp, 22, 4); // height = 1 + bmp[26] = 1; bmp[27] = 0; // planes = 1 + bmp[28] = 24; bmp[29] = 0; // bits per pixel = 24 + Buffer.BlockCopy(BitConverter.GetBytes(4u), 0, bmp, 34, 4); // image size = 4 + + // Pixel: BGR + 1 padding byte + bmp[54] = b; + bmp[55] = g; + bmp[56] = r; + + return bmp; + } + + // ─── tests ───────────────────────────────────────────────────────────────── + + [TestMethod] + public void ReadWrite_TwoStringResources_BothPresentWithCorrectKind() + { + string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(dir); + try + { + string resxBody = @" + Hello nanoFramework + Goodbye"; + + string outPath = RunOnResx(dir, resxBody); + + Assert.IsTrue(File.Exists(outPath), "nanoresources file was not created"); + + var nrf = ReadNanoresources(outPath); + Assert.AreEqual(2, nrf.resources.Length, "expected 2 resources"); + + foreach (var res in nrf.resources) + { + Assert.AreEqual( + ProcessResourceFiles.NanoResourceFile.ResourceHeader.RESOURCE_String, + res.header.kind, + "resource kind should be RESOURCE_String"); + } + + // String values present (order may differ by ID hash) + var strings = new string[2]; + for (int i = 0; i < 2; i++) + strings[i] = Encoding.UTF8.GetString(nrf.resources[i].data).TrimEnd('\0'); + + CollectionAssert.AreEquivalent( + new[] { "Hello nanoFramework", "Goodbye" }, + strings, + "string resource values do not match"); + } + finally + { + Directory.Delete(dir, true); + } + } + + [TestMethod] + public void ReadWrite_BinaryBlobFileRef_EmittedAsBinaryResource() + { + string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(dir); + try + { + byte[] blob = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; + File.WriteAllBytes(Path.Combine(dir, "data.bin"), blob); + + string resxBody = @" + + data.bin;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + "; + + string outPath = RunOnResx(dir, resxBody); + + Assert.IsTrue(File.Exists(outPath), "nanoresources file was not created"); + + var nrf = ReadNanoresources(outPath); + Assert.AreEqual(1, nrf.resources.Length, "expected 1 resource"); + Assert.AreEqual( + ProcessResourceFiles.NanoResourceFile.ResourceHeader.RESOURCE_Binary, + nrf.resources[0].header.kind, + "resource kind should be RESOURCE_Binary"); + + CollectionAssert.AreEqual(blob, nrf.resources[0].data, "binary blob content mismatch"); + } + finally + { + Directory.Delete(dir, true); + } + } + + [TestMethod] + public void ReadWrite_BitmapFileRef_EmittedAsBitmapResource() + { + string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(dir); + try + { + // Write a minimal 1×1 pure-red BMP (dither-safe colour: no half-level boundaries) + File.WriteAllBytes(Path.Combine(dir, "logo.bmp"), MakeOnePxBmp(r: 255, g: 0, b: 0)); + + string resxBody = @" + + logo.bmp;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + "; + + string outPath = RunOnResx(dir, resxBody); + + Assert.IsTrue(File.Exists(outPath), "nanoresources file was not created"); + + var nrf = ReadNanoresources(outPath); + Assert.AreEqual(1, nrf.resources.Length, "expected 1 resource"); + Assert.AreEqual( + ProcessResourceFiles.NanoResourceFile.ResourceHeader.RESOURCE_Bitmap, + nrf.resources[0].header.kind, + "resource kind should be RESOURCE_Bitmap"); + } + finally + { + Directory.Delete(dir, true); + } + } + + [TestMethod] + public void ReadWrite_DuplicateResourceName_OnlyOneIsEmitted() + { + // ResXResourceReader (net472) uses a Hashtable internally: the last value + // for a duplicate key overwrites previous ones at the reader level, so only + // one entry reaches AddResource. We verify the count is 1, not which value + // wins (that is an implementation detail of the underlying reader). + string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(dir); + try + { + string resxBody = @" + first value + second value"; + + string outPath = RunOnResx(dir, resxBody); + + var nrf = ReadNanoresources(outPath); + Assert.AreEqual(1, nrf.resources.Length, "duplicate should be deduplicated — expected 1 resource"); + } + finally + { + Directory.Delete(dir, true); + } + } + + [TestMethod] + public void ReadWrite_StringAndBinaryMixed_BothEmittedWithCorrectKinds() + { + string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(dir); + try + { + byte[] blob = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }; + File.WriteAllBytes(Path.Combine(dir, "blob.bin"), blob); + + string resxBody = @" + nanoFramework + + blob.bin;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + "; + + string outPath = RunOnResx(dir, resxBody); + + var nrf = ReadNanoresources(outPath); + Assert.AreEqual(2, nrf.resources.Length, "expected 2 resources"); + + bool hasString = false, hasBinary = false; + foreach (var res in nrf.resources) + { + if (res.header.kind == ProcessResourceFiles.NanoResourceFile.ResourceHeader.RESOURCE_String) + hasString = true; + else if (res.header.kind == ProcessResourceFiles.NanoResourceFile.ResourceHeader.RESOURCE_Binary) + hasBinary = true; + } + + Assert.IsTrue(hasString, "no RESOURCE_String found"); + Assert.IsTrue(hasBinary, "no RESOURCE_Binary found"); + } + finally + { + Directory.Delete(dir, true); + } + } + + [TestMethod] + public void ReadWrite_SameInputs_ProduceDeterministicOutput() + { + // Regression guard: identical inputs must always produce byte-identical output. + string dir1 = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + string dir2 = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(dir1); + Directory.CreateDirectory(dir2); + try + { + byte[] blob = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + File.WriteAllBytes(Path.Combine(dir1, "data.bin"), blob); + File.WriteAllBytes(Path.Combine(dir2, "data.bin"), blob); + + string resxBody = @" + SmokeApp + + data.bin;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + "; + + byte[] out1 = File.ReadAllBytes(RunOnResx(dir1, resxBody)); + byte[] out2 = File.ReadAllBytes(RunOnResx(dir2, resxBody)); + + CollectionAssert.AreEqual(out1, out2, "identical inputs must produce identical output"); + } + finally + { + Directory.Delete(dir1, true); + Directory.Delete(dir2, true); + } + } + } +} + +#endif diff --git a/test/BuildTasks.Tests/Rgb565ConversionTests.cs b/test/BuildTasks.Tests/Rgb565ConversionTests.cs new file mode 100644 index 0000000..5ddbe39 --- /dev/null +++ b/test/BuildTasks.Tests/Rgb565ConversionTests.cs @@ -0,0 +1,228 @@ +// +// Copyright (c) .NET Foundation and Contributors +// See LICENSE file in the project root for full license information. +// + +// CrossPlatformBitmap is only compiled on non-Framework targets. +#if !NETFRAMEWORK + +using System; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using nanoFramework.Tools; + +namespace nanoFramework.Tools.BuildTasks.Tests +{ + /// + /// Unit tests for : validates that the + /// ImageSharp-backed RGB565 converter produces the correct 16-bit pixel values + /// for known input colours, and that bitmap format detection works correctly. + /// + [TestClass] + public class Rgb565ConversionTests + { + // ─── CLR_GFX_BitmapDescription layout ──────────────────────────────────── + // + // GenerateBitmapResourceData returns: + // [12 bytes: CLR_GFX_BitmapDescription serialised] + // [width * height * 2 bytes: RGB565 pixel data, little-endian] + // + // BitmapDescription.Serialize writes: + // m_width (uint32) = 4 bytes + // m_height (uint32) = 4 bytes + // m_flags (uint16) = 2 bytes + // m_bitsPerPixel (byte) = 1 byte + // m_type (byte) = 1 byte + // ──────────────────────────────── + // Total = 12 bytes + + private const int BitmapDescriptionSize = 12; + + // ─── helpers ─────────────────────────────────────────────────────────────── + + /// + /// Creates a minimal valid 24bpp 1×1 BMP with the given RGB pixel. + /// BMP stores pixels as BGR, bottom-up, rows padded to 4-byte boundary. + /// + private static byte[] MakeOnePxBmp(byte r, byte g, byte b) + { + // 14-byte file header + 40-byte BITMAPINFOHEADER + 4-byte pixel row + var bmp = new byte[58]; + + // File header + bmp[0] = 0x42; bmp[1] = 0x4D; + Buffer.BlockCopy(BitConverter.GetBytes(58u), 0, bmp, 2, 4); // file size + Buffer.BlockCopy(BitConverter.GetBytes(54u), 0, bmp, 10, 4); // pixel data offset + + // BITMAPINFOHEADER + Buffer.BlockCopy(BitConverter.GetBytes(40u), 0, bmp, 14, 4); // header size + Buffer.BlockCopy(BitConverter.GetBytes(1u), 0, bmp, 18, 4); // width = 1 + Buffer.BlockCopy(BitConverter.GetBytes(1u), 0, bmp, 22, 4); // height = 1 + bmp[26] = 1; bmp[27] = 0; // planes = 1 + bmp[28] = 24; bmp[29] = 0; // bits per pixel = 24 + Buffer.BlockCopy(BitConverter.GetBytes(4u), 0, bmp, 34, 4); // image size = 4 + + // Pixel data: BGR order + 1 padding byte (row must be 4-byte aligned) + bmp[54] = b; + bmp[55] = g; + bmp[56] = r; + + return bmp; + } + + /// + /// Calls on a + /// 1×1 BMP and returns the resulting RGB565 value. + /// + private static ushort ConvertPixelToRgb565(byte r, byte g, byte b) + { + byte[] bmpBytes = MakeOnePxBmp(r, g, b); + byte[] resourceData = CrossPlatformBitmap.GenerateBitmapResourceData(bmpBytes); + + // Pixel immediately follows the 12-byte description + Assert.IsTrue(resourceData.Length >= BitmapDescriptionSize + 2, + "resource data too short"); + + return (ushort)(resourceData[BitmapDescriptionSize] | + (resourceData[BitmapDescriptionSize + 1] << 8)); + } + + // ─── RGB565 pixel conversion tests ──────────────────────────────────────── + // + // Quantisation rules (from ProcessResourceFiles.CrossPlatform.cs): + // r5 = min(31, (R + 4) >> 3) ← round-to-nearest (5 bits) + // g6 = G >> 2 ← truncate (6 bits) + // b5 = min(31, (B + 4) >> 3) ← round-to-nearest (5 bits) + // RGB565 = (r5 << 11) | (g6 << 5) | b5 + + [TestMethod] + public void Convert_PureBlack_YieldsZero() + { + ushort rgb565 = ConvertPixelToRgb565(0, 0, 0); + Assert.AreEqual(0x0000, rgb565, "black should be 0x0000"); + } + + [TestMethod] + public void Convert_PureWhite_YieldsAllOnes() + { + // R=255 → r5=min(31,32)=31, G=255 → g6=63, B=255 → b5=31 → 0xFFFF + ushort rgb565 = ConvertPixelToRgb565(255, 255, 255); + Assert.AreEqual(0xFFFF, rgb565, "white should be 0xFFFF"); + } + + [TestMethod] + public void Convert_PureRed_YieldsExpectedValue() + { + // R=255 → r5=31, G=0 → g6=0, B=0 → b5=0 → 0xF800 + ushort rgb565 = ConvertPixelToRgb565(255, 0, 0); + Assert.AreEqual(0xF800, rgb565, "pure red should be 0xF800"); + } + + [TestMethod] + public void Convert_PureGreen_YieldsExpectedValue() + { + // R=0 → r5=0, G=255 → g6=63, B=0 → b5=0 → 0x07E0 + ushort rgb565 = ConvertPixelToRgb565(0, 255, 0); + Assert.AreEqual(0x07E0, rgb565, "pure green should be 0x07E0"); + } + + [TestMethod] + public void Convert_PureBlue_YieldsExpectedValue() + { + // R=0 → r5=0, G=0 → g6=0, B=255 → b5=31 → 0x001F + ushort rgb565 = ConvertPixelToRgb565(0, 0, 255); + Assert.AreEqual(0x001F, rgb565, "pure blue should be 0x001F"); + } + + [TestMethod] + public void Convert_Yellow_YieldsExpectedValue() + { + // R=255 → r5=31, G=255 → g6=63, B=0 → b5=0 → 0xFFE0 + ushort rgb565 = ConvertPixelToRgb565(255, 255, 0); + Assert.AreEqual(0xFFE0, rgb565, "yellow should be 0xFFE0"); + } + + [TestMethod] + public void Convert_Cyan_YieldsExpectedValue() + { + // R=0 → r5=0, G=255 → g6=63, B=255 → b5=31 → 0x07FF + ushort rgb565 = ConvertPixelToRgb565(0, 255, 255); + Assert.AreEqual(0x07FF, rgb565, "cyan should be 0x07FF"); + } + + [TestMethod] + public void Convert_Magenta_YieldsExpectedValue() + { + // R=255 → r5=31, G=0 → g6=0, B=255 → b5=31 → 0xF81F + ushort rgb565 = ConvertPixelToRgb565(255, 0, 255); + Assert.AreEqual(0xF81F, rgb565, "magenta should be 0xF81F"); + } + + [TestMethod] + public void Convert_MidGray_ChannelsQuantisedIndependently() + { + // R=128 → (128+4)>>3 = 16, G=128 → 128>>2 = 32, B=128 → 16 + // RGB565 = (16<<11)|(32<<5)|16 = 0x8410 + ushort rgb565 = ConvertPixelToRgb565(128, 128, 128); + Assert.AreEqual(0x8410, rgb565, "mid-gray quantisation mismatch"); + } + + // ─── bitmap format detection tests ──────────────────────────────────────── + + [TestMethod] + public void IsSupportedBitmap_ValidBmp_ReturnsTrue() + { + byte[] bmp = MakeOnePxBmp(255, 0, 0); + Assert.IsTrue(CrossPlatformBitmap.IsSupportedBitmap(bmp), "1×1 BMP should be detected"); + } + + [TestMethod] + public void IsSupportedBitmap_PngBytes_ReturnsFalse() + { + // Minimal PNG magic bytes — not in the supported set (nanoFramework rejects PNG) + byte[] pngMagic = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + Assert.IsFalse(CrossPlatformBitmap.IsSupportedBitmap(pngMagic), "PNG should not be supported"); + } + + [TestMethod] + public void IsSupportedBitmap_RandomBytes_ReturnsFalse() + { + byte[] garbage = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; + Assert.IsFalse(CrossPlatformBitmap.IsSupportedBitmap(garbage), "garbage bytes should not be supported"); + } + + [TestMethod] + public void IsSupportedBitmap_EmptyArray_ReturnsFalse() + { + Assert.IsFalse(CrossPlatformBitmap.IsSupportedBitmap(Array.Empty()), "empty array should not be supported"); + } + + // ─── resource data structure tests ──────────────────────────────────────── + + [TestMethod] + public void GenerateBitmapResourceData_OnePxBmp_HasCorrectDimensions() + { + byte[] bmpBytes = MakeOnePxBmp(0, 0, 0); + byte[] data = CrossPlatformBitmap.GenerateBitmapResourceData(bmpBytes); + + Assert.AreEqual(BitmapDescriptionSize + 2, data.Length, + "1×1 16bpp BMP should produce 12 (description) + 2 (pixel) bytes"); + + // m_width at offset 0: uint32 little-endian + uint width = BitConverter.ToUInt32(data, 0); + Assert.AreEqual(1u, width, "width should be 1"); + + // m_height at offset 4: uint32 little-endian + uint height = BitConverter.ToUInt32(data, 4); + Assert.AreEqual(1u, height, "height should be 1"); + + // m_bitsPerPixel at offset 10 + Assert.AreEqual(16, data[10], "bits per pixel should be 16 for BMP"); + + // m_type at offset 11 (0 = c_TypeBitmap) + Assert.AreEqual(0, data[11], "type should be c_TypeBitmap (0)"); + } + } +} + +#endif diff --git a/test/SmokeTest/Resources.resx b/test/SmokeTest/Resources.resx new file mode 100644 index 0000000..dd9391b --- /dev/null +++ b/test/SmokeTest/Resources.resx @@ -0,0 +1,48 @@ + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + Hello .NET nanoFramework! + + + v1.0.0 + + + + + logo.bmp;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + data.bin;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + diff --git a/test/SmokeTest/SmokeTest.csproj b/test/SmokeTest/SmokeTest.csproj index c324e91..0b1990c 100644 --- a/test/SmokeTest/SmokeTest.csproj +++ b/test/SmokeTest/SmokeTest.csproj @@ -9,10 +9,12 @@ true + true + diff --git a/test/SmokeTest/data.bin b/test/SmokeTest/data.bin new file mode 100644 index 0000000..bf5af90 --- /dev/null +++ b/test/SmokeTest/data.bin @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/SmokeTest/logo.bmp b/test/SmokeTest/logo.bmp new file mode 100644 index 0000000000000000000000000000000000000000..3db2c732cc9e448c5f61fc267397875c17fd9ba9 GIT binary patch literal 118 zcmZ?rEn|QHGa#h_#4J$E$e;jZLBJ6{1_lsD0{=k($cNHEHV^^@z Date: Wed, 24 Jun 2026 18:40:23 +0100 Subject: [PATCH 2/2] Fixes from code review --- .../Sdk/nanoFramework.Resources.targets | 2 +- .../GenerateBinaryOutputTask.cs | 20 ++++++-- .../GenerateNanoResourceTask.cs | 4 ++ .../ProcessResourceFiles.cs | 18 ++++++- .../Infrastructure/TestBuildEngine.cs | 2 +- .../NanoResxResourceReaderTests.cs | 48 +++++++++++++------ .../ProcessResourceFilesTests.cs | 4 +- 7 files changed, 74 insertions(+), 24 deletions(-) diff --git a/nanoFramework.NET.Sdk/Sdk/nanoFramework.Resources.targets b/nanoFramework.NET.Sdk/Sdk/nanoFramework.Resources.targets index a1cea5a..c0aaacc 100644 --- a/nanoFramework.NET.Sdk/Sdk/nanoFramework.Resources.targets +++ b/nanoFramework.NET.Sdk/Sdk/nanoFramework.Resources.targets @@ -75,9 +75,9 @@ peCollection = new List(); - peCollection = AssemblyReferences?.Select(a => { return a.GetMetadata("FullPath").Replace(".dll", ".pe").Replace(".exe", ".pe"); }).ToList(); + List peCollection = AssemblyReferences? + .Select(a => Path.ChangeExtension(a.GetMetadata("FullPath"), ".pe")) + .ToList() ?? new List(); // add executable PE peCollection.Add(AssemblyPE); @@ -68,8 +68,20 @@ public override bool Execute() { long length = (fs.Length + 3) / 4 * 4; byte[] buffer = new byte[length]; + int fileLength = (int)fs.Length; + int totalRead = 0; - fs.Read(buffer, 0, (int)fs.Length); + while (totalRead < fileLength) + { + int bytesRead = fs.Read(buffer, totalRead, fileLength - totalRead); + + if (bytesRead == 0) + { + throw new EndOfStreamException($"Unexpected end of file while reading '{peItem}'."); + } + + totalRead += bytesRead; + } // copy this assembly to the bin file too binFile.Write(buffer, 0, (int)length); diff --git a/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.cs b/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.cs index e0dca48..e33715e 100644 --- a/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.cs +++ b/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.cs @@ -844,7 +844,11 @@ public override bool Execute() AssemblyName[] assemblyList; try { //only load system.drawing, mscorlib. no parameters needed here?!! +#if NETFRAMEWORK assemblyList = LoadReferences(); +#else + assemblyList = Array.Empty(); +#endif } catch (ArgumentException e) { diff --git a/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.cs b/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.cs index 58ab97a..2da0f5c 100644 --- a/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.cs +++ b/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.cs @@ -138,7 +138,7 @@ internal void Run(TaskLoggingHelper log, AssemblyName[] assemblies, ITaskItem[] { if (!ProcessFile(inFiles[i], outFiles[i])) { - UnsuccessfullyCreatedOutFiles.Add(outFiles[i]); + UnsuccessfullyCreatedOutFiles.Add(outFiles[i].ItemSpec); } } } @@ -1841,6 +1841,12 @@ public void Serialize(BinaryWriter writer) Resource resource = resources[iResource]; resource.header.Serialize(writer); + } + + for (int iResource = 0; iResource < resources.Length; iResource++) + { + Resource resource = resources[iResource]; + writer.Write(resource.data); } } @@ -1863,6 +1869,14 @@ public void Deserialize(BinaryReader reader) resources[iResource] = resource; resource.header.Deserialize(reader); + } + + reader.BaseStream.Position = header.OffsetOfResourceData; + + for (int iResource = 0; iResource < resources.Length; iResource++) + { + Resource resource = resources[iResource]; + resource.data = reader.ReadBytes((int)resource.header.size); } } @@ -2109,7 +2123,7 @@ void IResourceWriter.Generate() file.AddResource(new NanoResourceFile.Resource(resource, data)); } - using (FileStream fileStream = File.Open(fileName, FileMode.OpenOrCreate)) + using (FileStream fileStream = File.Open(fileName, FileMode.Create)) { BinaryWriter writer = new BinaryWriter(fileStream); file.Serialize(writer); diff --git a/test/BuildTasks.Tests/Infrastructure/TestBuildEngine.cs b/test/BuildTasks.Tests/Infrastructure/TestBuildEngine.cs index 598b180..c777b98 100644 --- a/test/BuildTasks.Tests/Infrastructure/TestBuildEngine.cs +++ b/test/BuildTasks.Tests/Infrastructure/TestBuildEngine.cs @@ -41,7 +41,7 @@ public bool BuildProjectFile(string projectFileName, string[] targetNames, /// internal sealed class TestTask : ITask { - public IBuildEngine BuildEngine { get; set; } = null!; + public IBuildEngine BuildEngine { get; set; } = new TestBuildEngine(); public ITaskHost? HostObject { get; set; } public bool Execute() => true; } diff --git a/test/BuildTasks.Tests/NanoResxResourceReaderTests.cs b/test/BuildTasks.Tests/NanoResxResourceReaderTests.cs index 0da7211..61526c0 100644 --- a/test/BuildTasks.Tests/NanoResxResourceReaderTests.cs +++ b/test/BuildTasks.Tests/NanoResxResourceReaderTests.cs @@ -32,11 +32,31 @@ public class NanoResxResourceReaderTests private const string ResxFooter = "\n"; - private static NanoResxResourceReader ParseResx(string resxBody, string? basePath = null) + private sealed class ParsedResx : IDisposable + { + private readonly StringReader _source; + + internal ParsedResx(string xml, string? basePath) + { + _source = new StringReader(xml); + Reader = new NanoResxResourceReader(_source, basePath ?? Path.GetTempPath()); + } + + internal NanoResxResourceReader Reader { get; } + + public static implicit operator NanoResxResourceReader(ParsedResx parsed) => parsed.Reader; + + public void Dispose() + { + Reader.Dispose(); + _source.Dispose(); + } + } + + private static ParsedResx ParseResx(string resxBody, string? basePath = null) { string xml = ResxHeader + resxBody + ResxFooter; - using var sr = new StringReader(xml); - return new NanoResxResourceReader(sr, basePath ?? Path.GetTempPath()); + return new ParsedResx(xml, basePath); } private static System.Collections.Hashtable ToHashtable(NanoResxResourceReader reader) @@ -53,7 +73,7 @@ private static System.Collections.Hashtable ToHashtable(NanoResxResourceReader r [TestMethod] public void Parse_InlineString_ReturnsStringValue() { - var reader = ParseResx(@" + using var reader = ParseResx(@" Hello nanoFramework"); var resources = ToHashtable(reader); @@ -66,7 +86,7 @@ public void Parse_InlineString_ReturnsStringValue() [TestMethod] public void Parse_MultipleInlineStrings_AllPresent() { - var reader = ParseResx(@" + using var reader = ParseResx(@" alpha beta gamma"); @@ -82,7 +102,7 @@ public void Parse_MultipleInlineStrings_AllPresent() [TestMethod] public void Parse_EmptyStringValue_ReturnsEmptyString() { - var reader = ParseResx(@" + using var reader = ParseResx(@" "); var resources = ToHashtable(reader); @@ -95,7 +115,7 @@ public void Parse_EmptyStringValue_ReturnsEmptyString() public void Parse_MissingNameAttribute_EntryIsSkipped() { // An entry without a 'name' attribute should be silently ignored. - var reader = ParseResx(@" + using var reader = ParseResx(@" no name ok"); @@ -113,7 +133,7 @@ public void Parse_InlineBase64ByteArray_ReturnsByteArray() byte[] expected = new byte[] { 0x01, 0x02, 0x03, 0x04 }; string base64 = Convert.ToBase64String(expected); - var reader = ParseResx($@" + using var reader = ParseResx($@" {base64} "); @@ -130,7 +150,7 @@ public void Parse_UnsupportedMimetype_ThrowsNotSupportedException() { Assert.ThrowsException(() => { - var reader = ParseResx(@" + using var reader = ParseResx(@" AQID "); @@ -150,7 +170,7 @@ public void Parse_FileRef_ByteArray_ReturnsByteArray() byte[] expected = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }; File.WriteAllBytes(Path.Combine(dir, "blob.bin"), expected); - var reader = ParseResx(@" + using var reader = ParseResx(@" blob.bin;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ", basePath: dir); @@ -177,7 +197,7 @@ public void Parse_FileRef_String_ReturnsStringContent() const string content = "Hello from file"; File.WriteAllText(Path.Combine(dir, "text.txt"), content, Encoding.UTF8); - var reader = ParseResx(@" + using var reader = ParseResx(@" text.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ", basePath: dir); @@ -207,7 +227,7 @@ public void Parse_FileRef_AbsolutePath_Resolved() // Use an absolute path in the file ref value string absPathEscaped = absPath.Replace("\\", "/"); - var reader = ParseResx($@" + using var reader = ParseResx($@" {absPath};System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ", basePath: null); @@ -229,7 +249,7 @@ public void Parse_UnsupportedType_ThrowsNotSupportedException() // A resource with an explicit non-file-ref, non-string type is unsupported. Assert.ThrowsException(() => { - var reader = ParseResx(@" + using var reader = ParseResx(@" AQID "); @@ -240,7 +260,7 @@ public void Parse_UnsupportedType_ThrowsNotSupportedException() public void Parse_ResxHeaderEntries_Ignored() { // nodes must not appear in the resource enumeration. - var reader = ParseResx(@" + using var reader = ParseResx(@" hello"); var resources = ToHashtable(reader); diff --git a/test/BuildTasks.Tests/ProcessResourceFilesTests.cs b/test/BuildTasks.Tests/ProcessResourceFilesTests.cs index ae5f77d..c340ab9 100644 --- a/test/BuildTasks.Tests/ProcessResourceFilesTests.cs +++ b/test/BuildTasks.Tests/ProcessResourceFilesTests.cs @@ -27,8 +27,8 @@ namespace nanoFramework.Tools.BuildTasks.Tests /// These tests use the public ReadResources / WriteResources API /// rather than Run() so that the test project has no runtime dependency on /// Microsoft.Build.Utilities.Core (whose assembly is not on the default - /// probe path when running dotnet test). Both TFMs (net8.0 and net472) are - /// exercised. + /// probe path when running dotnet test). This file is compiled and run only + /// for NETFRAMEWORK (net472). /// [TestClass] public class ProcessResourceFilesTests