From 5fe38d6aaf4773dbe34444b222e5db16f0c63019 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 10 Jul 2017 12:32:06 +0200 Subject: [PATCH] Build - Verify NuGet --- .../Umbraco.Build/Get-UmbracoBuildEnv.ps1 | 10 + .../Umbraco.Build/Get-UmbracoVersion.ps1 | 10 - .../Modules/Umbraco.Build/Umbraco.Build.psm1 | 3 + build/Modules/Umbraco.Build/Verify-NuGet.ps1 | 434 ++++++++++++++++++ build/build.md | 6 + src/Umbraco.Tests/Dependencies/NuGet.cs | 236 ---------- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - 7 files changed, 453 insertions(+), 247 deletions(-) create mode 100644 build/Modules/Umbraco.Build/Verify-NuGet.ps1 delete mode 100644 src/Umbraco.Tests/Dependencies/NuGet.cs diff --git a/build/Modules/Umbraco.Build/Get-UmbracoBuildEnv.ps1 b/build/Modules/Umbraco.Build/Get-UmbracoBuildEnv.ps1 index c58517bdd0..28a1f4aa5e 100644 --- a/build/Modules/Umbraco.Build/Get-UmbracoBuildEnv.ps1 +++ b/build/Modules/Umbraco.Build/Get-UmbracoBuildEnv.ps1 @@ -81,6 +81,16 @@ function Get-UmbracoBuildEnv mv "$file" $semver Remove-Directory $dir } + + try + { + [Reflection.Assembly]::LoadFile($semver) > $null + } + catch + { + Write-Error -Exception $_.Exception -Message "Failed to load $semver" + break + } # ensure we have node $node = "$path\node-v6.9.1-win-x86" diff --git a/build/Modules/Umbraco.Build/Get-UmbracoVersion.ps1 b/build/Modules/Umbraco.Build/Get-UmbracoVersion.ps1 index 25d73df976..a3ce784f14 100644 --- a/build/Modules/Umbraco.Build/Get-UmbracoVersion.ps1 +++ b/build/Modules/Umbraco.Build/Get-UmbracoVersion.ps1 @@ -6,16 +6,6 @@ function Get-UmbracoVersion { $uenv = Get-UmbracoBuildEnv - try - { - [Reflection.Assembly]::LoadFile($uenv.Semver) > $null - } - catch - { - Write-Error -Exception $_.Exception -Message "Failed to load $uenv.Semver" - break - } - # parse SolutionInfo and retrieve the version string $filepath = "$($uenv.SolutionRoot)\src\SolutionInfo.cs" $text = [System.IO.File]::ReadAllText($filepath) diff --git a/build/Modules/Umbraco.Build/Umbraco.Build.psm1 b/build/Modules/Umbraco.Build/Umbraco.Build.psm1 index 75e4064031..b89aec6e8c 100644 --- a/build/Modules/Umbraco.Build/Umbraco.Build.psm1 +++ b/build/Modules/Umbraco.Build/Umbraco.Build.psm1 @@ -20,6 +20,7 @@ . "$PSScriptRoot\Get-UmbracoBuildEnv.ps1" . "$PSScriptRoot\Set-UmbracoVersion.ps1" . "$PSScriptRoot\Get-UmbracoVersion.ps1" +. "$PSScriptRoot\Verify-NuGet.ps1" . "$PSScriptRoot\Build-UmbracoDocs.ps1" @@ -534,6 +535,7 @@ function Build-Umbraco # not running tests... Prepare-Packages $uenv Package-Zip $uenv + Verify-NuGet $uenv Prepare-NuGet $uenv Package-NuGet $uenv $version } @@ -551,5 +553,6 @@ Export-ModuleMember -function Set-UmbracoVersion Export-ModuleMember -function Get-UmbracoVersion Export-ModuleMember -function Build-Umbraco Export-ModuleMember -function Build-UmbracoDocs +Export-ModuleMember -function Verify-NuGet #eof \ No newline at end of file diff --git a/build/Modules/Umbraco.Build/Verify-NuGet.ps1 b/build/Modules/Umbraco.Build/Verify-NuGet.ps1 new file mode 100644 index 0000000000..811919cc52 --- /dev/null +++ b/build/Modules/Umbraco.Build/Verify-NuGet.ps1 @@ -0,0 +1,434 @@ +# +# Verify-NuGet +# + +function Format-Dependency +{ + param ( $d ) + + $m = $d.Id + " " + if ($d.MinInclude) { $m = $m + "[" } + else { $m = $m + "(" } + $m = $m + $d.MinVersion + if ($d.MaxVersion -ne $d.MinVersion) { $m = $m + "," + $d.MaxVersion } + if ($d.MaxInclude) { $m = $m + "]" } + else { $m = $m + ")" } + + return $m +} + +function Write-NuSpec +{ + param ( $name, $deps ) + + Write-Host "" + Write-Host "$name NuSpec dependencies:" + + foreach ($d in $deps) + { + $m = Format-Dependency $d + Write-Host " $m" + } +} + +function Write-Package +{ + param ( $name, $pkgs ) + + Write-Host "" + Write-Host "$name packages:" + + foreach ($p in $pkgs) + { + Write-Host " $($p.Id) $($p.Version)" + } +} + +function Verify-NuGet +{ + param ( + $uenv # an Umbraco build environment (see Get-UmbracoBuildEnv) + ) + + if ($uenv -eq $null) + { + $uenv = Get-UmbracoBuildEnv + } + + $source = @" + + using System; + using System.Collections.Generic; + using System.Linq; + using System.IO; + using System.Xml; + using System.Xml.Serialization; + using Semver; + + namespace Umbraco.Build + { + public class NuGet + { + public static Dependency[] GetNuSpecDependencies(string filename) + { + NuSpec nuspec; + var serializer = new XmlSerializer(typeof(NuSpec)); + using (var reader = new StreamReader(filename)) + { + nuspec = (NuSpec) serializer.Deserialize(reader); + } + var nudeps = nuspec.Metadata.Dependencies; + var deps = new List(); + foreach (var nudep in nudeps) + { + var dep = new Dependency(); + dep.Id = nudep.Id; + + var parts = nudep.Version.Split(','); + if (parts.Length == 1) + { + dep.MinInclude = parts[0].StartsWith("["); + dep.MaxInclude = parts[0].EndsWith("]"); + + SemVersion version; + if (!SemVersion.TryParse(parts[0].Substring(1, parts[0].Length-2).Trim(), out version)) continue; + dep.MinVersion = dep.MaxVersion = version; //parts[0].Substring(1, parts[0].Length-2).Trim(); + } + else + { + SemVersion version; + if (!SemVersion.TryParse(parts[0].Substring(1).Trim(), out version)) continue; + dep.MinVersion = version; //parts[0].Substring(1).Trim(); + if (!SemVersion.TryParse(parts[1].Substring(0, parts[1].Length-1).Trim(), out version)) continue; + dep.MaxVersion = version; //parts[1].Substring(0, parts[1].Length-1).Trim(); + dep.MinInclude = parts[0].StartsWith("["); + dep.MaxInclude = parts[1].EndsWith("]"); + } + + deps.Add(dep); + } + return deps.ToArray(); + } + + public static IEnumerable DistinctBy(/*this*/ IEnumerable source, Func keySelector) + { + HashSet knownKeys = new HashSet(); + foreach (TSource element in source) + { + if (knownKeys.Add(keySelector(element))) + { + yield return element; + } + } + } + + public static Package[] GetProjectsPackages(string src, string[] projects) + { + var l = new List(); + foreach (var project in projects) + { + var path = Path.Combine(src, project); + var packageConfig = Path.Combine(path, "packages.config"); + if (File.Exists(packageConfig)) + ReadPackagesConfig(packageConfig, l); + var csprojs = Directory.GetFiles(path, "*.csproj"); + foreach (var csproj in csprojs) + { + ReadCsProj(csproj, l); + } + } + IEnumerable p = l.OrderBy(x => x.Id); + p = DistinctBy(p, x => x.Id + ":::" + x.Version); + return p.ToArray(); + } + + public static object[] GetPackageErrors(Package[] pkgs) + { + return pkgs + .GroupBy(x => x.Id) + .Where(x => x.Count() > 1) + .ToArray(); + } + + public static object[] GetNuSpecErrors(Package[] pkgs, Dependency[] deps) + { + var d = pkgs.ToDictionary(x => x.Id, x => x.Version); + return deps + .Select(x => + { + SemVersion v; + if (!d.TryGetValue(x.Id, out v)) return null; + + var ok = true; + + /* + if (x.MinInclude) + { + if (v < x.MinVersion) ok = false; + } + else + { + if (v <= x.MinVersion) ok = false; + } + + if (x.MaxInclude) + { + if (v > x.MaxVersion) ok = false; + } + else + { + if (v >= x.MaxVersion) ok = false; + } + */ + + if (!x.MinInclude || v != x.MinVersion) ok = false; + + return ok ? null : new { Dependency = x, Version = v }; + }) + .Where(x => x != null) + .ToArray(); + } + + /* + public static Package[] GetProjectPackages(string path) + { + var l = new List(); + var packageConfig = Path.Combine(path, "packages.config"); + if (File.Exists(packageConfig)) + ReadPackagesConfig(packageConfig, l); + var csprojs = Directory.GetFiles(path, "*.csproj"); + foreach (var csproj in csprojs) + { + ReadCsProj(csproj, l); + } + return l.ToArray(); + } + */ + + public static string GetDirectoryName(string filename) + { + return Path.GetFileName(Path.GetDirectoryName(filename)); + } + + public static void ReadPackagesConfig(string filename, List packages) + { + //Console.WriteLine("read " + filename); + + PackagesConfigPackages pkgs; + var serializer = new XmlSerializer(typeof(PackagesConfigPackages)); + using (var reader = new StreamReader(filename)) + { + pkgs = (PackagesConfigPackages) serializer.Deserialize(reader); + } + foreach (var p in pkgs.Packages) + { + SemVersion version; + if (!SemVersion.TryParse(p.Version, out version)) continue; + packages.Add(new Package { Id = p.Id, Version = version, Project = GetDirectoryName(filename) }); + } + } + + public static void ReadCsProj(string filename, List packages) + { + //Console.WriteLine("read " + filename); + + // if xmlns then it's not a VS2017 with PackageReference + var text = File.ReadAllLines(filename); + var line = text.FirstOrDefault(x => x.Contains(" x.Packages != null).SelectMany(x => x.Packages)) + { + var sversion = p.VersionE ?? p.VersionA; + SemVersion version; + if (!SemVersion.TryParse(sversion, out version)) continue; + packages.Add(new Package { Id = p.Id, Version = version, Project = GetDirectoryName(filename) }); + } + } + + public class Dependency + { + public string Id { get; set; } + public SemVersion MinVersion { get; set; } + public SemVersion MaxVersion { get; set; } + public bool MinInclude { get; set; } + public bool MaxInclude { get; set; } + } + + public class Package + { + public string Id { get; set; } + public SemVersion Version { get; set; } + public string Project { get; set; } + } + + [XmlType(AnonymousType = true, Namespace = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd")] + [XmlRoot(Namespace = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd", IsNullable = false, ElementName = "package")] + public class NuSpec + { + [XmlElement("metadata")] + public NuSpecMetadata Metadata { get; set; } + } + + [XmlType(AnonymousType = true, Namespace = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd", TypeName = "metadata")] + public class NuSpecMetadata + { + [XmlArray("dependencies")] + [XmlArrayItem("dependency", IsNullable = false)] + public NuSpecDependency[] Dependencies { get; set; } + } + + [XmlType(AnonymousType = true, Namespace = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd", TypeName = "dependencies")] + public class NuSpecDependency + { + [XmlAttribute(AttributeName = "id")] + public string Id { get; set; } + + [XmlAttribute(AttributeName = "version")] + public string Version { get; set; } + } + + [XmlType(AnonymousType = true)] + [XmlRoot(Namespace = "", IsNullable = false, ElementName = "packages")] + public class PackagesConfigPackages + { + [XmlElement("package")] + public PackagesConfigPackage[] Packages { get; set; } + } + + [XmlType(AnonymousType = true, TypeName = "package")] + public class PackagesConfigPackage + { + [XmlAttribute(AttributeName = "id")] + public string Id { get; set; } + + [XmlAttribute(AttributeName = "version")] + public string Version { get; set; } + } + + [XmlType(AnonymousType = true)] + [XmlRoot(Namespace = "", IsNullable = false, ElementName = "Project")] + public class CsProjProject + { + [XmlElement("ItemGroup")] + public CsProjItemGroup[] ItemGroups { get; set; } + } + + [XmlType(AnonymousType = true, TypeName = "ItemGroup")] + public class CsProjItemGroup + { + [XmlElement("PackageReference")] + public CsProjPackageReference[] Packages { get; set; } + } + + [XmlType(AnonymousType = true, TypeName = "PackageReference")] + public class CsProjPackageReference + { + [XmlAttribute(AttributeName = "Include")] + public string Id { get; set; } + + [XmlAttribute(AttributeName = "Version")] + public string VersionA { get; set; } + + [XmlElement("Version")] + public string VersionE { get; set;} + } + } + } + +"@ + + Write-Host "Verify NuGet consistency" + + $assem = ( + "System.Xml", + "System.Core", # "System.Collections.Generic" + "System.Linq", + "System.Xml.Serialization", + "System.IO", + "System.Globalization", + $uenv.Semver + ) + + add-type -referencedAssemblies $assem -typeDefinition $source -language CSharp + if (-not $?) { break } + + $nuspecs = ( + "UmbracoCms", + "UmbracoCms.Core", + "UmbracoCms.Compat7" + ) + + $projects = ( + "Umbraco.Core", + "Umbraco.Web", + "Umbraco.Web.UI", + "UmbracoExamine", + "Umbraco.Compat7"#, + #"Umbraco.Tests", + #"Umbraco.Tests.Benchmarks" + ) + + $src = "$($uenv.SolutionRoot)\src" + $pkgs = [Umbraco.Build.NuGet]::GetProjectsPackages($src, $projects) + if (-not $?) { break } + #Write-Package "All" $pkgs + + $errs = [Umbraco.Build.NuGet]::GetPackageErrors($pkgs) + if (-not $?) { break } + + if ($errs.Length -gt 0) + { + Write-Host "" + } + foreach ($err in $errs) + { + Write-Host $err.Key + foreach ($e in $err) + { + Write-Host " $($e.Version) required by $($e.Project)" + } + } + if ($errs.Length -gt 0) + { + Write-Error "Found non-consolidated package dependencies" + break + } + + $nuerr = $false + $nupath = "$($uenv.SolutionRoot)\build\NuSpecs" + foreach ($nuspec in $nuspecs) + { + $deps = [Umbraco.Build.NuGet]::GetNuSpecDependencies("$nupath\$nuspec.nuspec") + if (-not $?) { break } + #Write-NuSpec $nuspec $deps + + $errs = [Umbraco.Build.NuGet]::GetNuSpecErrors($pkgs, $deps) + if (-not $?) { break } + + if ($errs.Length -gt 0) + { + Write-Host "" + Write-Host "$nuspec requires:" + $nuerr = $true + } + foreach ($err in $errs) + { + $m = Format-Dependency $err.Dependency + Write-Host " $m but projects require $($err.Version)" + } + } + + if ($nuerr) + { + Write-Error "Found inconsistent NuGet dependencies" + break + } +} \ No newline at end of file diff --git a/build/build.md b/build/build.md index 00d7624ecc..4fe80aca06 100644 --- a/build/build.md +++ b/build/build.md @@ -102,6 +102,12 @@ Builds umbraco documentation. Temporary files are generated in `build.tmp` while Some log files, such as MsBuild logs, are produced in `build.tmp` too. The `build` directory should remain clean during a build. +## Verify-NuGet + +Verifies that projects all require the same version of their dependencies, and that NuSpec files require versions that are consistent with projects. Example: + + Verify-NuGet + # VSTS Continuous integration, nightly builds and release builds run on VSTS. diff --git a/src/Umbraco.Tests/Dependencies/NuGet.cs b/src/Umbraco.Tests/Dependencies/NuGet.cs deleted file mode 100644 index 9818f18517..0000000000 --- a/src/Umbraco.Tests/Dependencies/NuGet.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Xml.Serialization; -using NUnit.Framework; -using Umbraco.Tests.TestHelpers; - -namespace Umbraco.Tests.Dependencies -{ - [TestFixture] - public class NuGet - { - // note - // these tests assume that the test suite runs from the project's ~/bin directory - // instead, we want to be able to pass the required paths as parameters - // NUnit 3.x supports TestContext.TestParameters, alas we are still running 2.x - // - // so instead - // - // furthermore, all these tests should be parts of the build and fail the build - // in case the NuGet things are not consisted. Also, v8 uses - // wherever possible so we will also have to deal with it eventually. - - [Test] - public void NuGet_Package_Versions_Are_The_Same_In_All_Package_Config_Files() - { - var packagesAndVersions = GetNuGetPackagesInSolution(); - Assert.IsTrue(packagesAndVersions.Any()); - - var failTest = false; - foreach (var package in packagesAndVersions.OrderBy(x => x.ConfigFilePath)) - { - var matchingPackages = packagesAndVersions.Where(x => string.Equals(x.PackageName, package.PackageName, StringComparison.InvariantCultureIgnoreCase)); - foreach (var matchingPackage in matchingPackages) - { - if (package.PackageVersion == matchingPackage.PackageVersion) - continue; - - Debug.WriteLine("Package '{0}' with version {1} in {2} doesn't match with version {3} in {4}", - package.PackageName, package.PackageVersion, package.ConfigFilePath, - matchingPackage.PackageVersion, matchingPackage.ConfigFilePath); - failTest = true; - } - } - - Assert.IsFalse(failTest); - } - - [Test] - public void NuSpec_File_Matches_Installed_Dependencies() - { - var nuspecsDirectory = NuSpecsDirectory; - Assert.IsNotNull(nuspecsDirectory); - - var packagesAndVersions = GetNuGetPackagesInSolution(); - Assert.IsTrue(packagesAndVersions.Any()); - - Console.WriteLine("NuSpecs: " + nuspecsDirectory.FullName); - - var failTest = false; - var nuspecFiles = nuspecsDirectory.GetFiles().Where(x => x.Extension == ".nuspec").Select(x => x.FullName); - foreach (var filename in nuspecFiles) - { - var serializer = new XmlSerializer(typeof(NuSpec)); - using (var reader = new StreamReader(filename)) - { - var nuspec = (NuSpec)serializer.Deserialize(reader); - var dependencies = nuspec.MetaData.dependencies; - - //UmbracoCms.Core has version "[$version$]" - ignore - foreach (var dependency in dependencies.Where(x => x.Id != "UmbracoCms.Core")) - { - var dependencyVersionRange = dependency.Version.Replace("[", string.Empty).Replace("(", string.Empty).Split(','); - var dependencyMinimumVersion = dependencyVersionRange.First().Trim(); - - var matchingPackages = packagesAndVersions.Where(x => string.Equals(x.PackageName, dependency.Id, - StringComparison.InvariantCultureIgnoreCase)).ToList(); - if (matchingPackages.Any() == false) - continue; - - // NuGet_Package_Versions_Are_The_Same_In_All_Package_Config_Files test - // guarantees that all packages have one version, solutionwide, so it's okay to take First() here - if (dependencyMinimumVersion != matchingPackages.First().PackageVersion) - { - Console.WriteLine("NuSpec dependency '{0}' with minimum version {1} doesn't match with version {2} in the solution", - dependency.Id, dependencyMinimumVersion, matchingPackages.First().PackageVersion); - failTest = true; - } - } - } - } - - Assert.IsFalse(failTest); - } - - private List GetNuGetPackagesInSolution() - { - var packagesAndVersions = new List(); - var sourceDirectory = SourceDirectory; - if (sourceDirectory == null) return packagesAndVersions; - - var packageConfigFiles = new List(); - var packageDirectories = - sourceDirectory.GetDirectories().Where(x => - x.Name.StartsWith("Umbraco.Tests") == false && - x.Name.StartsWith("Umbraco.MSBuild.Tasks") == false).ToArray(); - - foreach (var directory in packageDirectories) - { - var packagesFiles = directory.EnumerateFiles("packages.config"); - packageConfigFiles.AddRange(packagesFiles); - } - - foreach (var file in packageConfigFiles) - { - //read all and de-duplicate packages - var serializer = new XmlSerializer(typeof(PackageConfigEntries)); - using (var reader = new StreamReader(file.FullName)) - { - var packages = (PackageConfigEntries)serializer.Deserialize(reader); - packagesAndVersions.AddRange(packages.Package.Select(package => - new PackageVersionMatcher - { - PackageName = package.Id, - PackageVersion = package.Version, - ConfigFilePath = file.FullName - })); - } - } - return packagesAndVersions; - } - - private DirectoryInfo SourceDirectory - { - get - { - // UMBRACO_TMP is set by VSTS and points to build.tmp - var tmp = Environment.GetEnvironmentVariable("UMBRACO_TMP"); - if (tmp == null) return SolutionDirectory; - - var path = Path.Combine(tmp, "..\\src"); - path = Path.GetFullPath(path); // sanitize - var dir = new DirectoryInfo(path); - return dir.Exists ? dir : null; - } - } - - private DirectoryInfo NuSpecsDirectory - { - get - { - // UMBRACO_TMP is set by VSTS and points to build.tmp - var tmp = Environment.GetEnvironmentVariable("UMBRACO_TMP"); - if (tmp == null) - { - var solutionDirectory = SolutionDirectory; - if (solutionDirectory == null) return null; - return new DirectoryInfo(Path.Combine(solutionDirectory.FullName, "..\\build\\NuSpecs")); - } - - var path = Path.Combine(tmp, "..\\build\\NuSpecs"); - path = Path.GetFullPath(path); // sanitize - var dir = new DirectoryInfo(path); - return dir.Exists ? dir : null; - } - } - - private DirectoryInfo SolutionDirectory - { - get - { - var testsDirectory = new FileInfo(TestHelper.MapPathForTest("~/")); - if (testsDirectory.Directory == null) - return null; - if (testsDirectory.Directory.Parent == null || testsDirectory.Directory.Parent.Parent == null) - return null; - var solutionDirectory = testsDirectory.Directory.Parent.Parent.Parent; - return solutionDirectory; - } - } - } - - public class PackageVersionMatcher - { - public string ConfigFilePath { get; set; } - public string PackageName { get; set; } - public string PackageVersion { get; set; } - } - - [XmlType(AnonymousType = true)] - [XmlRoot(Namespace = "", IsNullable = false, ElementName = "packages")] - public class PackageConfigEntries - { - [XmlElement("package")] - public PackagesPackage[] Package { get; set; } - } - - [XmlType(AnonymousType = true, TypeName = "package")] - public class PackagesPackage - { - [XmlAttribute(AttributeName = "id")] - public string Id { get; set; } - - [XmlAttribute(AttributeName = "version")] - public string Version { get; set; } - } - - - [XmlType(AnonymousType = true, Namespace = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd")] - [XmlRoot(Namespace = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd", IsNullable = false, ElementName = "package")] - public class NuSpec - { - [XmlElement("metadata")] - public Metadata MetaData { get; set; } - } - - [XmlType(AnonymousType = true, Namespace = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd", TypeName = "metadata")] - public class Metadata - { - [XmlArrayItem("dependency", IsNullable = false)] - //TODO: breaks when renamed to capital D - public Dependency[] dependencies { get; set; } - } - - [XmlType(AnonymousType = true, Namespace = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd", TypeName = "dependencies")] - public class Dependency - { - [XmlAttribute(AttributeName = "id")] - public string Id { get; set; } - - [XmlAttribute(AttributeName = "version")] - public string Version { get; set; } - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 24243c3d46..3b29f4f99f 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -203,7 +203,6 @@ -