diff --git a/Examples/DotNetCoreBuild/validate-build.sh b/Examples/DotNetCoreBuild/validate-build.sh index 8433931d0..7987713ad 100755 --- a/Examples/DotNetCoreBuild/validate-build.sh +++ b/Examples/DotNetCoreBuild/validate-build.sh @@ -21,7 +21,8 @@ readonly FULLY_CACHED=0 readonly NOT_FULLY_CACHED=1 # this is the magic timestamp ("2003-03-03 3:03:03") translated to UTC Epoch seconds -readonly magicTimestamp=$(date -j -u -f "%Y-%m-%d %H:%M:%S" "2003-03-03 3:03:03" +%s) +# readonly magicTimestamp=$(date -j -u -f "%Y-%m-%d %H:%M:%S" "2003-03-03 3:03:03" +%s) +readonly magicXattrName="com.microsoft.buildxl:shared_opaque_output" function run_build_and_check_stuff { local expectGraphReloadedStatus=$1 @@ -58,16 +59,15 @@ function run_build_and_check_stuff { print_info $(if [[ $expectFullyCachedStatus == $FULLY_CACHED ]]; then echo "Verified build fully cached"; else echo "Verified build NOT fully cached"; fi) fi - # check that all files in all shared opaque directories (whose name is 'sod*') have the magic timestamp for 'Btime' + # check that all files in all shared opaque directories (whose name is 'sod*') have the magic xattr local sodFilesFile="$MY_DIR/sod-files.txt" find "$MY_DIR/out/objects" -type d -name 'sod*' -exec find {} -type f -o -type l \; > "$sodFilesFile" - local sodFilesTimestamps=$(cat "$sodFilesFile" | xargs stat -t "%s" -f "%SB" | sort | uniq) - if [[ $sodFilesTimestamps != $magicTimestamp ]]; then - print_error "Some files in the some shared output directories don't have the magic timestamp ('2003-03-03 3:03:03', i.e., $magicTimestamp) for Btime" - cat "$sodFilesFile" | xargs stat -t "%s" -f "Btime: %SB, path: %N" - rm -f "$sodFilesFile" - return 4 - fi + for f in `cat $sodFilesFile`; do + xattr -s "$f" | grep -q $magicXattrName || { + print_error "File '$f' does not have the magic xattr '$magicXattrName'" + return 4 + } + done rm -f "$sodFilesFile" } diff --git a/Public/Src/Engine/Processes/SharedOpaqueOutputHelper.cs b/Public/Src/Engine/Processes/SharedOpaqueOutputHelper.cs index efd2623cc..daabc88d7 100644 --- a/Public/Src/Engine/Processes/SharedOpaqueOutputHelper.cs +++ b/Public/Src/Engine/Processes/SharedOpaqueOutputHelper.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Runtime.InteropServices; using System.Security.AccessControl; using BuildXL.Native.IO; using BuildXL.Utilities; @@ -10,31 +11,152 @@ using static BuildXL.Utilities.FormattableStringEx; namespace BuildXL.Processes { /// - /// Utility class for identifying a file as being an output of a shared opaque. This helps the scrubber to be more precautious when deleting files under shared opaques. + /// Utility class for identifying a file as being an output of a shared opaque. + /// This helps the scrubber to be more precautious when deleting files under shared opaques. /// + /// + /// Currently, we use two different strategies when running on Windows and non-Windows platforms + /// - on Windows, we set a magic timestamp as file's creation (birth) date + /// - on Mac, we set an extended attribute with a special name. + /// On Mac, some tools tend to change the timestamps (even the birth date) which is the reason for this. + /// + /// TODO: eventually we should unify this and use the same mechanism for all platforms, if possible. + /// public static class SharedOpaqueOutputHelper { - /// - /// Flags the given path as being an output under a shared opaque by setting the creation time to - /// - /// When the timestamp cannot be set - public static void SetPathAsSharedOpaqueOutput(string expandedPath) + private static class Win { - try + /// + /// Flags the given path as being an output under a shared opaque by setting the creation time to + /// . + /// + /// When the timestamp cannot be set + public static void SetPathAsSharedOpaqueOutput(string expandedPath) { - // Only the creation time is used to identify a file as the output of a shared opaque - FileUtilities.SetFileTimestamps(expandedPath, new FileTimestamps(WellKnownTimestamps.OutputInSharedOpaqueTimestamp)); - } - catch (BuildXLException ex) - { - // On (unsafe) double writes, a race can occur, we give the other contender a second chance - if (IsSharedOpaqueOutput(expandedPath)) + // In the case of a no replay, this case can happen if the file got into the cache as a static output, + // but later was made a shared opaque output without a content change. + // Make sure we allow for attribute writing first + var writeAttributesDenied = !FileUtilities.HasWritableAttributeAccessControl(expandedPath); + if (writeAttributesDenied) { - return; + FileUtilities.SetFileAccessControl(expandedPath, FileSystemRights.WriteAttributes | FileSystemRights.WriteExtendedAttributes, allow: true); } - // Since these files should be just created outputs, this shouldn't happen and we bail out hard. - throw new BuildXLException(I($"Failed to open output file '{expandedPath}' for writing."), ex); + try + { + // Only the creation time is used to identify a file as the output of a shared opaque + FileUtilities.SetFileTimestamps(expandedPath, new FileTimestamps(WellKnownTimestamps.OutputInSharedOpaqueTimestamp)); + } + catch (BuildXLException ex) + { + // On (unsafe) double writes, a race can occur, we give the other contender a second chance + if (IsSharedOpaqueOutput(expandedPath)) + { + return; + } + + // Since these files should be just created outputs, this shouldn't happen and we bail out hard. + throw new BuildXLException(I($"Failed to open output file '{expandedPath}' for writing."), ex); + } + finally + { + // Restore the attributes as they were originally set + if (writeAttributesDenied) + { + FileUtilities.SetFileAccessControl(expandedPath, FileSystemRights.WriteAttributes | FileSystemRights.WriteExtendedAttributes, allow: false); + } + } + } + + /// + /// Checks if the given path is an output under a shared opaque by verifying whether is the creation time of the file + /// + /// + /// If the given path is a directory, it is always considered part of a shared opaque + /// + public static bool IsSharedOpaqueOutput(string expandedPath) + { + try + { + var creationTime = FileUtilities.GetFileTimestamps(expandedPath).CreationTime; + return creationTime == WellKnownTimestamps.OutputInSharedOpaqueTimestamp; + } + catch (BuildXLException ex) + { + throw new BuildXLException(I($"Failed to open output file '{expandedPath}' for reading."), ex); + } + } + } + + private static unsafe class Unix + { + private const string MY_XATTR_NAME = "com.microsoft.buildxl:shared_opaque_output"; + + // arbitrary value; in the future, we could store something more useful here (e.g., the producer PipId or something) + private const long MY_XATTR_VALUE = 42; + + // from xattr.h: + // #define XATTR_NOFOLLOW 0x0001 /* Don't follow symbolic links */ + private const int XATTR_NOFOLLOW = 1; + + [DllImport("libc", EntryPoint = "setxattr")] + private static extern int SetXattr( + [MarshalAs(UnmanagedType.LPStr)] string path, + [MarshalAs(UnmanagedType.LPStr)] string name, + void *value, + ulong size, + uint position, + int options); + + [DllImport("libc", EntryPoint = "getxattr")] + private static extern long GetXattr( + [MarshalAs(UnmanagedType.LPStr)] string path, + [MarshalAs(UnmanagedType.LPStr)] string name, + void *value, + ulong size, + uint position, + int options); + + /// + /// Flags the given path as being an output under a shared opaque by setting + /// xattr to a . + /// + public static void SetPathAsSharedOpaqueOutput(string expandedPath) + { + long value = MY_XATTR_VALUE; + var err = SetXattr(expandedPath, MY_XATTR_NAME, &value, sizeof(long), 0, XATTR_NOFOLLOW); + if (err != 0) + { + throw new BuildXLException(I($"Failed to set '{MY_XATTR_NAME}' extended attribute. Error: {err}")); + } + } + + /// + /// Checks if the given path is an output under a shared opaque by checking if + /// it contains extended attribute by name. + /// + public static bool IsSharedOpaqueOutput(string expandedPath) + { + long value = 0; + uint valueSize = sizeof(long); + var resultSize = GetXattr(expandedPath, MY_XATTR_NAME, &value, valueSize, 0, XATTR_NOFOLLOW); + return resultSize == valueSize && value == MY_XATTR_VALUE; + } + } + + /// + /// Marks a given path as "shared opaque output" + /// + /// When unsuccessful + public static void SetPathAsSharedOpaqueOutput(string expandedPath) + { + if (OperatingSystemHelper.IsUnixOS) + { + Unix.SetPathAsSharedOpaqueOutput(expandedPath); + } + else + { + Win.SetPathAsSharedOpaqueOutput(expandedPath); } } @@ -74,18 +196,9 @@ namespace BuildXL.Processes return true; } - DateTime creationTime; - - try - { - creationTime = FileUtilities.GetFileTimestamps(expandedPath).CreationTime; - } - catch (BuildXLException ex) - { - throw new BuildXLException(I($"Failed to open output file '{expandedPath}' for reading."), ex); - } - - return creationTime == WellKnownTimestamps.OutputInSharedOpaqueTimestamp; + return OperatingSystemHelper.IsUnixOS + ? Unix.IsSharedOpaqueOutput(expandedPath) + : Win.IsSharedOpaqueOutput(expandedPath); } /// @@ -93,37 +206,13 @@ namespace BuildXL.Processes /// public static void EnforceFileIsSharedOpaqueOutput(string expandedPath) { - // If the file has the right timestamps already, then there is nothing to do. + // If the file is already marked, then there is nothing to do. if (IsSharedOpaqueOutput(expandedPath)) { return; } - // In the case of a no replay, this case can happen if the file got into the cache as a static output, but later was made a shared opaque - // output without a content change. - -#if PLATFORM_WIN - // Make sure we allow for attribute writing first - var writeAttributesDenied = !FileUtilities.HasWritableAttributeAccessControl(expandedPath); - if (writeAttributesDenied) - { - FileUtilities.SetFileAccessControl(expandedPath, FileSystemRights.WriteAttributes | FileSystemRights.WriteExtendedAttributes, allow: true); - } -#endif - try - { - SetPathAsSharedOpaqueOutput(expandedPath); - } - finally - { -#if PLATFORM_WIN - // Restore the attributes as they were originally set - if (writeAttributesDenied) - { - FileUtilities.SetFileAccessControl(expandedPath, FileSystemRights.WriteAttributes | FileSystemRights.WriteExtendedAttributes, allow: false); - } -#endif - } + SetPathAsSharedOpaqueOutput(expandedPath); } } } diff --git a/Public/Src/Engine/UnitTests/Engine/SharedOpaqueEngineTests.cs b/Public/Src/Engine/UnitTests/Engine/SharedOpaqueEngineTests.cs index 235d8f15f..a1e57daa6 100644 --- a/Public/Src/Engine/UnitTests/Engine/SharedOpaqueEngineTests.cs +++ b/Public/Src/Engine/UnitTests/Engine/SharedOpaqueEngineTests.cs @@ -171,7 +171,7 @@ namespace Test.BuildXL.Engine } [Fact] - public void OutputsUnderSharedOpaqueHaveAWellKnownCreationTimeEvenOnCacheReplay() + public void OutputsUnderSharedOpaqueAreProperlyMarkedEvenOnCacheReplay() { var file = X("out/SharedOpaqueOutput.txt"); var spec0 = ProduceFileUnderSharedOpaque(file); @@ -185,8 +185,8 @@ namespace Test.BuildXL.Engine // Make sure the file was produced Assert.True(File.Exists(producedFile)); - // And that it has a well-known creation time - XAssert.AreEqual(WellKnownTimestamps.OutputInSharedOpaqueTimestamp, FileUtilities.GetFileTimestamps(producedFile).CreationTime); + // And that it has been marked as shared opaque output + XAssert.IsTrue(SharedOpaqueOutputHelper.IsSharedOpaqueOutput(producedFile)); File.Delete(producedFile); @@ -196,12 +196,12 @@ namespace Test.BuildXL.Engine IgnoreWarnings(); // Make sure this is a cache replay AssertVerboseEventLogged(EventId.ProcessPipCacheHit); - // And check the timestamp again - XAssert.AreEqual(WellKnownTimestamps.OutputInSharedOpaqueTimestamp, FileUtilities.GetFileTimestamps(producedFile).CreationTime); + // And check again that the file is still properly marked + XAssert.IsTrue(SharedOpaqueOutputHelper.IsSharedOpaqueOutput(producedFile)); } [Fact] - public void StaticOutputBecomingASharedOpaqueOutputHasWellKnownCreationTime() + public void StaticOutputBecomingASharedOpaqueOutputIsProperlyMarkedAsSharedOpaqueOutput() { var file = X("out/MyFile.txt"); @@ -216,8 +216,8 @@ namespace Test.BuildXL.Engine // Make sure the file was produced Assert.True(File.Exists(producedFile)); - // Since this is a statically declared file, the creation timestamp should not be the one used for shared opaques - XAssert.AreNotEqual(WellKnownTimestamps.OutputInSharedOpaqueTimestamp, FileUtilities.GetFileTimestamps(producedFile).CreationTime); + // Since this is a statically declared file, it shouldn't be marked as a shared opaque output + XAssert.IsFalse(SharedOpaqueOutputHelper.IsSharedOpaqueOutput(producedFile), "Statically declared file marked as shared opaque output"); // Delete the created file (since scrubbing is not on for this test, we have to simulate it) File.Delete(producedFile); @@ -230,7 +230,7 @@ namespace Test.BuildXL.Engine RunEngine(rememberAllChangedTrackedInputs: true); // Check the timestamp is the right one - XAssert.AreEqual(WellKnownTimestamps.OutputInSharedOpaqueTimestamp, FileUtilities.GetFileTimestamps(producedFile).CreationTime); + XAssert.IsTrue(SharedOpaqueOutputHelper.IsSharedOpaqueOutput(producedFile), "SOD file not marked on cache miss"); // Delete the file File.Delete(producedFile); @@ -242,11 +242,11 @@ namespace Test.BuildXL.Engine // Make sure this is a cache replay AssertVerboseEventLogged(EventId.ProcessPipCacheHit); // Check the timestamp is the right one now - XAssert.AreEqual(WellKnownTimestamps.OutputInSharedOpaqueTimestamp, FileUtilities.GetFileTimestamps(producedFile).CreationTime); + XAssert.IsTrue(SharedOpaqueOutputHelper.IsSharedOpaqueOutput(producedFile), "SOD file not marked on cache replay"); } [Fact] - public void SharedOpaqueOutputsOnFailingPipHaveWellKnownCreationTime() + public void SharedOpaqueOutputsOnFailingPipMustBeProperlyMarked() { var file = X("out/MyFile.txt"); var objDir = Configuration.Layout.ObjectDirectory.ToString(Context.PathTable); @@ -261,7 +261,7 @@ namespace Test.BuildXL.Engine AssertErrorEventLogged(EventId.PipProcessError); // Check the timestamp is the right one - XAssert.AreEqual(WellKnownTimestamps.OutputInSharedOpaqueTimestamp, FileUtilities.GetFileTimestamps(producedFile).CreationTime); + XAssert.IsTrue(SharedOpaqueOutputHelper.IsSharedOpaqueOutput(producedFile), "SOD file not marked on pip failure"); } private string ProduceFileUnderSharedOpaque(string file, bool failOnExit = false, string dependencies = "") => ProduceFileUnderDirectory(file, isDynamic: true, failOnExit, dependencies);