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);