Merge remote-tracking branch 'upstream/stabilization/2106' into cgalvan/gitflow_210811

Signed-off-by: Chris Galvan <chgalvan@amazon.com>
main
Chris Galvan 4 years ago
commit 575faa4443

@ -1,55 +0,0 @@
"""
Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
import os
import sys
import azlmbr.legacy.general as general
import azlmbr.paths
sys.path.append(os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Gem", "PythonTests"))
import editor_python_test_tools.hydra_editor_utils as hydra
from editor_python_test_tools.editor_test_helper import EditorTestHelper
class TestDebuggerDebugCVarsWorks(EditorTestHelper):
def __init__(self):
EditorTestHelper.__init__(self, log_prefix="Debugger_DebugCVarsWorks", args=["level"])
def run_test(self):
"""
Summary:
C2789148 Vegetation Debug CVars are enabled when the Debugger component is present
Expected Result:
The following commands are available in the Editor only when the Vegetation Debugger Level component is present:
veg_debugDumpReport (Command)
veg_debugRefreshAllAreas (Command)
:return: None
"""
# Create empty level
self.test_success = self.create_level(
self.args["level"],
heightmap_resolution=1024,
heightmap_meters_per_pixel=1,
terrain_texture_resolution=4096,
use_terrain=False,
)
# Initially run the command in console without Debugger component
general.run_console("veg_debugDumpReport")
# Add the Vegetation Debugger component to the Level Inspector
hydra.add_level_component("Vegetation Debugger")
# Run a command again after adding the Vegetation debugger
general.run_console("veg_debugRefreshAllAreas")
test = TestDebuggerDebugCVarsWorks()
test.run()

@ -1,56 +0,0 @@
"""
Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
import os
import pytest
import logging
# Bail on the test if ly_test_tools doesn't exist.
pytest.importorskip("ly_test_tools")
import ly_test_tools.environment.file_system as file_system
import editor_python_test_tools.hydra_test_utils as hydra
logger = logging.getLogger(__name__)
test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts")
@pytest.mark.parametrize("project", ["AutomatedTesting"])
@pytest.mark.parametrize("level", ["tmp_level"])
@pytest.mark.usefixtures("automatic_process_killer")
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
class TestDebugger(object):
@pytest.fixture(autouse=True)
def setup_teardown(self, request, workspace, project, level):
# Cleanup our temp level
file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True)
def teardown():
# Cleanup our temp level
file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True)
request.addfinalizer(teardown)
@pytest.mark.test_case_id("C2789148")
@pytest.mark.SUITE_periodic
@pytest.mark.dynveg_misc
def test_Debugger_DebugCVarsWork(self, request, editor, level, workspace, launcher_platform):
cfg_args = [level]
expected_lines = [
"Debugger_DebugCVarsWorks: test started",
"[Warning] Unknown command: veg_debugDumpReport",
"[CONSOLE] Executing console command 'veg_debugRefreshAllAreas'",
"Debugger_DebugCVarsWorks: result=SUCCESS",
]
hydra.launch_and_validate_results(
request,
test_directory,
editor,
"Debugger_DebugCVarsWorks.py",
expected_lines=expected_lines,
cfg_args=cfg_args
)

@ -104,6 +104,7 @@ class TestGeneralGraphFunctionality(object):
@pytest.mark.test_case_id("C17488412") @pytest.mark.test_case_id("C17488412")
@pytest.mark.SUITE_periodic @pytest.mark.SUITE_periodic
@pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/2201")
def test_LandscapeCanvas_GraphClosed_OnEntityDelete(self, request, editor, level, launcher_platform): def test_LandscapeCanvas_GraphClosed_OnEntityDelete(self, request, editor, level, launcher_platform):
cfg_args = [level] cfg_args = [level]

@ -163,6 +163,8 @@ ly_add_target(
editor_files.cmake editor_files.cmake
PLATFORM_INCLUDE_FILES PLATFORM_INCLUDE_FILES
Platform/${PAL_PLATFORM_NAME}/editor_${PAL_PLATFORM_NAME_LOWERCASE}.cmake Platform/${PAL_PLATFORM_NAME}/editor_${PAL_PLATFORM_NAME_LOWERCASE}.cmake
TARGET_PROPERTIES
LY_INSTALL_GENERATE_RUN_TARGET TRUE
BUILD_DEPENDENCIES BUILD_DEPENDENCIES
PRIVATE PRIVATE
3rdParty::Qt::Core 3rdParty::Qt::Core

@ -277,7 +277,7 @@ namespace AZ::IO
//! then their hash values are also equal //! then their hash values are also equal
//! For example : path "a//b" equals "a/b", the //! For example : path "a//b" equals "a/b", the
//! hash value of "a//b" would also equal the hash value of "a/b" //! hash value of "a//b" would also equal the hash value of "a/b"
constexpr size_t hash_value(const PathView& pathToHash) noexcept; size_t hash_value(const PathView& pathToHash) noexcept;
// path.comparison // path.comparison
constexpr bool operator==(const PathView& lhs, const PathView& rhs) noexcept; constexpr bool operator==(const PathView& lhs, const PathView& rhs) noexcept;
@ -622,7 +622,7 @@ namespace AZ::IO
//! For example : path "a//b" equals "a/b", the //! For example : path "a//b" equals "a/b", the
//! hash value of "a//b" would also equal the hash value of "a/b" //! hash value of "a//b" would also equal the hash value of "a/b"
template <typename StringType> template <typename StringType>
constexpr size_t hash_value(const BasicPath<StringType>& pathToHash); size_t hash_value(const BasicPath<StringType>& pathToHash);
// path.append // path.append
template <typename StringType> template <typename StringType>

@ -1951,7 +1951,7 @@ namespace AZ::IO
} }
template <typename StringType> template <typename StringType>
constexpr size_t hash_value(const BasicPath<StringType>& pathToHash) inline size_t hash_value(const BasicPath<StringType>& pathToHash)
{ {
return AZStd::hash<BasicPath<StringType>>{}(pathToHash); return AZStd::hash<BasicPath<StringType>>{}(pathToHash);
} }
@ -2082,13 +2082,28 @@ namespace AZStd
template <> template <>
struct hash<AZ::IO::PathView> struct hash<AZ::IO::PathView>
{ {
constexpr size_t operator()(const AZ::IO::PathView& pathToHash) noexcept /// Path is using FNV-1a algorithm 64 bit version.
static size_t hash_path(AZStd::string_view pathSegment, const char pathSeparator)
{
size_t hash = 14695981039346656037ULL;
constexpr size_t fnvPrime = 1099511628211ULL;
for (const char first : pathSegment)
{
hash ^= static_cast<size_t>((pathSeparator == AZ::IO::PosixPathSeparator)
? first : tolower(first));
hash *= fnvPrime;
}
return hash;
}
size_t operator()(const AZ::IO::PathView& pathToHash) noexcept
{ {
auto pathParser = AZ::IO::parser::PathParser::CreateBegin(pathToHash.Native(), pathToHash.m_preferred_separator); auto pathParser = AZ::IO::parser::PathParser::CreateBegin(pathToHash.Native(), pathToHash.m_preferred_separator);
size_t hash_value = 0; size_t hash_value = 0;
while (pathParser) while (pathParser)
{ {
AZStd::hash_combine(hash_value, AZStd::hash<AZStd::string_view>{}(*pathParser)); AZStd::hash_combine(hash_value, hash_path(*pathParser, pathToHash.m_preferred_separator));
++pathParser; ++pathParser;
} }
return hash_value; return hash_value;
@ -2097,7 +2112,7 @@ namespace AZStd
template <typename StringType> template <typename StringType>
struct hash<AZ::IO::BasicPath<StringType>> struct hash<AZ::IO::BasicPath<StringType>>
{ {
constexpr size_t operator()(const AZ::IO::BasicPath<StringType>& pathToHash) noexcept const size_t operator()(const AZ::IO::BasicPath<StringType>& pathToHash) noexcept
{ {
return AZStd::hash<AZ::IO::PathView>{}(pathToHash); return AZStd::hash<AZ::IO::PathView>{}(pathToHash);
} }
@ -2108,11 +2123,11 @@ namespace AZStd
template struct hash<AZ::IO::FixedMaxPath>; template struct hash<AZ::IO::FixedMaxPath>;
} }
// Explicit instantations of our support Path classes // Explicit instantiations of our support Path classes
namespace AZ::IO namespace AZ::IO
{ {
// PathView hash // PathView hash
constexpr size_t hash_value(const PathView& pathToHash) noexcept inline size_t hash_value(const PathView& pathToHash) noexcept
{ {
return AZStd::hash<PathView>{}(pathToHash); return AZStd::hash<PathView>{}(pathToHash);
} }

@ -182,6 +182,36 @@ namespace UnitTest
AZStd::tuple<AZStd::string_view, AZStd::string_view>(R"(foO/Bar)", "foo/bar") AZStd::tuple<AZStd::string_view, AZStd::string_view>(R"(foO/Bar)", "foo/bar")
)); ));
using PathHashParamFixture = PathParamFixture;
TEST_P(PathHashParamFixture, HashOperator_HashesCaseInsensitiveForWindowsPaths)
{
AZ::IO::Path path1{ AZStd::get<0>(GetParam()), AZ::IO::WindowsPathSeparator };
AZ::IO::Path path2{ AZStd::get<1>(GetParam()), AZ::IO::WindowsPathSeparator };
size_t path1Hash = AZStd::hash<AZ::IO::PathView>{}(path1);
size_t path2Hash = AZStd::hash<AZ::IO::PathView>{}(path2);
EXPECT_EQ(path1Hash, path2Hash) << AZStd::string::format(R"(path1 "%s" should hash to path2 "%s"\n)",
path1.c_str(), path2.c_str()).c_str();
}
TEST_P(PathHashParamFixture, HashOperator_HashesCaseSensitiveForPosixPaths)
{
AZ::IO::Path path1{ AZStd::get<0>(GetParam()), AZ::IO::PosixPathSeparator };
AZ::IO::Path path2{ AZStd::get<1>(GetParam()), AZ::IO::PosixPathSeparator };
size_t path1Hash = AZStd::hash<AZ::IO::PathView>{}(path1);
size_t path2Hash = AZStd::hash<AZ::IO::PathView>{}(path2);
EXPECT_NE(path1Hash, path2Hash) << AZStd::string::format(R"(path1 "%s" should NOT hash to path2 "%s"\n)",
path1.c_str(), path2.c_str()).c_str();
}
INSTANTIATE_TEST_CASE_P(
HashPaths,
PathHashParamFixture,
::testing::Values(
AZStd::tuple<AZStd::string_view, AZStd::string_view>("C:/test/foo", R"(c:\test/foo)"),
AZStd::tuple<AZStd::string_view, AZStd::string_view>(R"(D:\test/bar/baz//foo)", "d:/test/bar/baz\\\\\\foo"),
AZStd::tuple<AZStd::string_view, AZStd::string_view>(R"(foO/Bar)", "foo/bar")
));
class PathSingleParamFixture class PathSingleParamFixture
: public ScopedAllocatorSetupFixture : public ScopedAllocatorSetupFixture
, public ::testing::WithParamInterface<AZStd::tuple<AZStd::string_view>> , public ::testing::WithParamInterface<AZStd::tuple<AZStd::string_view>>

@ -206,12 +206,12 @@ namespace AzToolsFramework
const AZStd::string& AssetBrowserEntry::GetRelativePath() const const AZStd::string& AssetBrowserEntry::GetRelativePath() const
{ {
return m_relativePath; return m_relativePath.Native();
} }
const AZStd::string& AssetBrowserEntry::GetFullPath() const const AZStd::string& AssetBrowserEntry::GetFullPath() const
{ {
return m_fullPath; return m_fullPath.Native();
} }
const AssetBrowserEntry* AssetBrowserEntry::GetChild(int index) const const AssetBrowserEntry* AssetBrowserEntry::GetChild(int index) const

@ -9,6 +9,7 @@
#if !defined(Q_MOC_RUN) #if !defined(Q_MOC_RUN)
#include <AzCore/std/string/string.h> #include <AzCore/std/string/string.h>
#include <AzCore/Asset/AssetCommon.h> #include <AzCore/Asset/AssetCommon.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Math/Uuid.h> #include <AzCore/Math/Uuid.h>
#include <AzToolsFramework/Thumbnails/Thumbnail.h> #include <AzToolsFramework/Thumbnails/Thumbnail.h>
@ -130,8 +131,8 @@ namespace AzToolsFramework
protected: protected:
AZStd::string m_name; AZStd::string m_name;
QString m_displayName; QString m_displayName;
AZStd::string m_relativePath; AZ::IO::Path m_relativePath;
AZStd::string m_fullPath; AZ::IO::Path m_fullPath;
AZStd::vector<AssetBrowserEntry*> m_children; AZStd::vector<AssetBrowserEntry*> m_children;
AssetBrowserEntry* m_parentAssetEntry = nullptr; AssetBrowserEntry* m_parentAssetEntry = nullptr;

@ -37,8 +37,8 @@ namespace AzToolsFramework
void FolderAssetBrowserEntry::UpdateChildPaths(AssetBrowserEntry* child) const void FolderAssetBrowserEntry::UpdateChildPaths(AssetBrowserEntry* child) const
{ {
child->m_relativePath = m_relativePath + AZ_CORRECT_DATABASE_SEPARATOR + child->m_name; child->m_relativePath = m_relativePath / child->m_name;
child->m_fullPath = m_fullPath + AZ_CORRECT_DATABASE_SEPARATOR + child->m_name; child->m_fullPath = m_fullPath / child->m_name;
AssetBrowserEntry::UpdateChildPaths(child); AssetBrowserEntry::UpdateChildPaths(child);
} }

@ -10,6 +10,7 @@
#include <AzFramework/StringFunc/StringFunc.h> #include <AzFramework/StringFunc/StringFunc.h>
#include <AzCore/IO/FileIO.h> #include <AzCore/IO/FileIO.h>
#include <AzCore/IO/Path/Path.h>
#include <AzToolsFramework/AssetBrowser/Entries/RootAssetBrowserEntry.h> #include <AzToolsFramework/AssetBrowser/Entries/RootAssetBrowserEntry.h>
#include <AzToolsFramework/AssetBrowser/Entries/FolderAssetBrowserEntry.h> #include <AzToolsFramework/AssetBrowser/Entries/FolderAssetBrowserEntry.h>
@ -24,8 +25,6 @@ namespace AzToolsFramework
{ {
namespace AssetBrowser namespace AssetBrowser
{ {
const char* GEMS_FOLDER_NAME = "Gems";
RootAssetBrowserEntry::RootAssetBrowserEntry() RootAssetBrowserEntry::RootAssetBrowserEntry()
: AssetBrowserEntry() : AssetBrowserEntry()
{ {
@ -53,13 +52,7 @@ namespace AzToolsFramework
EntryCache::GetInstance()->Clear(); EntryCache::GetInstance()->Clear();
m_enginePath = enginePath; m_enginePath = enginePath;
m_fullPath = enginePath;
// there is no "Gems" scan folder registered in db, create one manually
auto gemFolder = aznew FolderAssetBrowserEntry();
gemFolder->m_name = m_enginePath + AZ_CORRECT_DATABASE_SEPARATOR + GEMS_FOLDER_NAME;
gemFolder->m_displayName = GEMS_FOLDER_NAME;
gemFolder->m_isGemsFolder = true;
AddChild(gemFolder);
} }
bool RootAssetBrowserEntry::IsInitialUpdate() const bool RootAssetBrowserEntry::IsInitialUpdate() const
@ -80,8 +73,17 @@ namespace AzToolsFramework
if (AZ::IO::FileIOBase::GetInstance()->IsDirectory(scanFolderDatabaseEntry.m_scanFolder.c_str())) if (AZ::IO::FileIOBase::GetInstance()->IsDirectory(scanFolderDatabaseEntry.m_scanFolder.c_str()))
{ {
const auto scanFolder = CreateFolders(scanFolderDatabaseEntry.m_scanFolder.c_str(), this); const auto scanFolder = CreateFolders(scanFolderDatabaseEntry.m_scanFolder, this);
scanFolder->m_displayName = QString::fromUtf8(scanFolderDatabaseEntry.m_displayName.c_str()); // Append an "[External]" to the display if the Scan Folder is NOT relative to the Engine Root path
if (!AZ::IO::PathView(scanFolderDatabaseEntry.m_scanFolder).IsRelativeTo(m_enginePath))
{
scanFolder->m_displayName += " [External]";
}
else
{
scanFolder->m_displayName = QString::fromUtf8(scanFolderDatabaseEntry.m_displayName.c_str());
}
EntryCache::GetInstance()->m_scanFolderIdMap[scanFolderDatabaseEntry.m_scanFolderID] = scanFolder; EntryCache::GetInstance()->m_scanFolderIdMap[scanFolderDatabaseEntry.m_scanFolderID] = scanFolder;
} }
} }
@ -121,38 +123,34 @@ namespace AzToolsFramework
return; return;
} }
const char* filePath = fileDatabaseEntry.m_fileName.c_str(); AZ::IO::FixedMaxPath absoluteFilePath = AZ::IO::FixedMaxPath(AZStd::string_view{ scanFolder->GetFullPath() })
/ fileDatabaseEntry.m_fileName.c_str();
AssetBrowserEntry* file; AssetBrowserEntry* file;
// file can be either folder or actual file // file can be either folder or actual file
if (fileDatabaseEntry.m_isFolder) if (fileDatabaseEntry.m_isFolder)
{ {
file = CreateFolders(filePath, scanFolder); file = CreateFolders(absoluteFilePath.Native(), scanFolder);
} }
else else
{ {
AZStd::string sourcePath;
AZStd::string sourceName;
AZStd::string sourceExtension;
StringFunc::Path::Split(filePath, nullptr, &sourcePath, &sourceName, &sourceExtension);
// if missing create folders leading to file's location and get immediate parent // if missing create folders leading to file's location and get immediate parent
// (we don't need to have fileIds for any folders created yet, they will be added later) // (we don't need to have fileIds for any folders created yet, they will be added later)
auto parent = CreateFolders(sourcePath.c_str(), scanFolder); auto parent = CreateFolders(absoluteFilePath.ParentPath().Native(), scanFolder);
// for simplicity in AB, files are represented as sources, but they are missing SourceDatabaseEntry-specific information such as SourceUuid // for simplicity in AB, files are represented as sources, but they are missing SourceDatabaseEntry-specific information such as SourceUuid
auto source = aznew SourceAssetBrowserEntry(); auto source = aznew SourceAssetBrowserEntry();
source->m_name = (sourceName + sourceExtension).c_str(); source->m_name = absoluteFilePath.Filename().Native();
source->m_fileId = fileDatabaseEntry.m_fileID; source->m_fileId = fileDatabaseEntry.m_fileID;
source->m_displayName = QString::fromUtf8(source->m_name.c_str()); source->m_displayName = QString::fromUtf8(source->m_name.c_str());
source->m_scanFolderId = fileDatabaseEntry.m_scanFolderPK; source->m_scanFolderId = fileDatabaseEntry.m_scanFolderPK;
source->m_extension = sourceExtension.c_str(); source->m_extension = absoluteFilePath.Extension().Native();
parent->AddChild(source); parent->AddChild(source);
file = source; file = source;
} }
EntryCache::GetInstance()->m_fileIdMap[fileDatabaseEntry.m_fileID] = file; EntryCache::GetInstance()->m_fileIdMap[fileDatabaseEntry.m_fileID] = file;
AZStd::string fullPath = file->m_fullPath; AZStd::string filePath = AZ::IO::PathView(file->m_fullPath).LexicallyNormal().String();
AzFramework::StringFunc::Path::Normalize(fullPath); EntryCache::GetInstance()->m_absolutePathToFileId[filePath] = fileDatabaseEntry.m_fileID;
EntryCache::GetInstance()->m_absolutePathToFileId[fullPath] = fileDatabaseEntry.m_fileID;
} }
bool RootAssetBrowserEntry::RemoveFile(const AZ::s64& fileId) const bool RootAssetBrowserEntry::RemoveFile(const AZ::s64& fileId) const
@ -305,116 +303,95 @@ namespace AzToolsFramework
} }
} }
FolderAssetBrowserEntry* RootAssetBrowserEntry::CreateFolder(const char* folderName, AssetBrowserEntry* parent) AssetBrowserEntry* RootAssetBrowserEntry::GetNearestAncestor(AZ::IO::PathView absolutePathView, AssetBrowserEntry* parent,
AZStd::unordered_set<AssetBrowserEntry*>& visitedSet)
{ {
auto it = AZStd::find_if(parent->m_children.begin(), parent->m_children.end(), [folderName](AssetBrowserEntry* entry) auto IsPathRelativeToEntry = [absolutePathView](AssetBrowserEntry* assetBrowserEntry)
{
if (!azrtti_istypeof<FolderAssetBrowserEntry*>(entry))
{
return false;
}
return AzFramework::StringFunc::Equal(entry->m_name.c_str(), folderName);
});
if (it != parent->m_children.end())
{ {
return azrtti_cast<FolderAssetBrowserEntry*>(*it); auto& childPath = assetBrowserEntry->m_fullPath;
return absolutePathView.IsRelativeTo(AZ::IO::PathView(childPath));
};
if (visitedSet.contains(parent))
{
return {};
} }
const auto folder = aznew FolderAssetBrowserEntry();
folder->m_name = folderName;
folder->m_displayName = folderName;
parent->AddChild(folder);
return folder;
}
AssetBrowserEntry* RootAssetBrowserEntry::CreateFolders(const char* relativePath, AssetBrowserEntry* parent) visitedSet.insert(parent);
{
auto children(parent->m_children);
int n = 0;
// check if folder with the same name already exists AssetBrowserEntry* nearestAncestor{};
// step through every character in relativePath and compare to each child's relative path of suggested parent for (AssetBrowserEntry* childBrowserEntry : parent->m_children)
// if a character @n in child's rel path mismatches character at n in relativePath, remove that child from further search
while (!children.empty() && relativePath[n])
{ {
AZStd::vector<AssetBrowserEntry*> toRemove; if (IsPathRelativeToEntry(childBrowserEntry))
for (auto child : children)
{ {
auto& childPath = azrtti_istypeof<RootAssetBrowserEntry*>(parent) ? child->m_fullPath : child->m_relativePath; // Walk the AssetBrowserEntry Tree looking for a nearer ancestor to the absolute path
// If one is not found in the recursive call to GetNearestAncestor, then the childBrowserEntry
// child's path mismatched, remove it from search candidates // is the current best candidate
if (childPath.length() == n || childPath[n] != relativePath[n]) AssetBrowserEntry* candidateAncestor = GetNearestAncestor(absolutePathView, childBrowserEntry, visitedSet);
candidateAncestor = candidateAncestor != nullptr ? candidateAncestor : childBrowserEntry;
AZ::IO::PathView candidatePathView(candidateAncestor->m_fullPath);
// If the candidate is relative to the current nearest ancestor, then it is even nearer to the path
if (!nearestAncestor || candidatePathView.IsRelativeTo(nearestAncestor->m_fullPath))
{ {
toRemove.push_back(child); nearestAncestor = candidateAncestor;
// If the full path compares equal to the AssetBrowserEntry path, then no need to proceed any further
// it is possible that child may be a closer parent, substitute it as new potential parent if (AZ::IO::PathView(nearestAncestor->m_fullPath) == absolutePathView)
// e.g. child->m_relativePath = 'Gems', relativePath = 'Gems/Assets', old parent = root, new parent = Gems
if (childPath.length() == n && relativePath[n] == AZ_CORRECT_DATABASE_SEPARATOR)
{ {
parent = child; break;
relativePath += n; // advance relative path n characters since the parent has changed
n = 0; // Once the relative path pointer is advanced, reset n
} }
} }
} }
for (auto entry : toRemove)
{
children.erase(AZStd::remove(children.begin(), children.end(), entry), children.end());
}
n++;
} }
// filter out the remaining children that don't end with '/' or '\0' return nearestAncestor;
// for example if folderName = "foo", while children may still remain with names like "foo123", }
// which is not the same folder
AZStd::vector<AssetBrowserEntry*> toRemove; FolderAssetBrowserEntry* RootAssetBrowserEntry::CreateFolder(AZStd::string_view folderName, AssetBrowserEntry* parent)
for (auto child : children) {
auto it = AZStd::find_if(parent->m_children.begin(), parent->m_children.end(), [folderName](AssetBrowserEntry* entry)
{ {
auto& childPath = azrtti_istypeof<RootAssetBrowserEntry*>(parent) ? child->m_fullPath : child->m_relativePath; if (!azrtti_istypeof<FolderAssetBrowserEntry*>(entry))
// check if there are non-null characters remaining @n
if (childPath.length() > n)
{ {
toRemove.push_back(child); return false;
} }
} return AZ::IO::PathView(entry->m_name) == AZ::IO::PathView(folderName);
for (auto entry : toRemove) });
if (it != parent->m_children.end())
{ {
children.erase(AZStd::remove(children.begin(), children.end(), entry), children.end()); return azrtti_cast<FolderAssetBrowserEntry*>(*it);
} }
const auto folder = aznew FolderAssetBrowserEntry();
folder->m_name = folderName;
folder->m_displayName = QString::fromUtf8(folderName.data(), aznumeric_caster(folderName.size()));
parent->AddChild(folder);
return folder;
}
AssetBrowserEntry* RootAssetBrowserEntry::CreateFolders(AZStd::string_view absolutePath, AssetBrowserEntry* parent)
{
AZ::IO::PathView absolutePathView(absolutePath);
// Find the nearest ancestor path to the absolutePath
AZStd::unordered_set<AssetBrowserEntry*> visitedSet;
// at least one child remains, this means the folder with this name already exists, return it if (AssetBrowserEntry* nearestAncestor = GetNearestAncestor(absolutePathView, parent, visitedSet);
if (!children.empty()) nearestAncestor != nullptr)
{ {
parent = children.front(); parent = nearestAncestor;
} }
// if it's a scanfolder, then do not create folders leading to it
// e.g. instead of 'C:\dev\SampleProject' just create 'SampleProject' // If the nearest ancestor is the absolutePath, then it is already crated
else if (parent->GetEntryType() == AssetEntryType::Root) if (absolutePathView == AZ::IO::PathView(parent->GetFullPath()))
{ {
AZStd::string folderName; return parent;
AzFramework::StringFunc::Path::Split(relativePath, nullptr, nullptr, &folderName);
parent = CreateFolder(folderName.c_str(), parent);
parent->m_fullPath = relativePath;
} }
// otherwise create all missing folders
else // create all missing folders
auto proximateToPath = absolutePathView.IsRelativeTo(parent->m_fullPath)
? absolutePathView.LexicallyProximate(parent->m_fullPath)
: AZ::IO::FixedMaxPath(absolutePathView);
for (AZ::IO::FixedMaxPath scanFolderSegment : proximateToPath)
{ {
n = 0; parent = CreateFolder(scanFolderSegment.c_str(), parent);
AZStd::string folderName(strlen(relativePath) + 1, '\0');
// iterate through relativePath until the first '/'
while (relativePath[n] && relativePath[n] != AZ_CORRECT_DATABASE_SEPARATOR)
{
folderName[n] = relativePath[n];
n++;
}
if (n > 0)
{
parent = CreateFolder(folderName.c_str(), parent);
}
// n+1 also skips the '/' character
if (relativePath[n] && relativePath[n + 1])
{
parent = CreateFolders(relativePath + n + 1, parent);
}
} }
return parent; return parent;
} }
@ -422,7 +399,7 @@ namespace AzToolsFramework
void RootAssetBrowserEntry::UpdateChildPaths(AssetBrowserEntry* child) const void RootAssetBrowserEntry::UpdateChildPaths(AssetBrowserEntry* child) const
{ {
child->m_relativePath = child->m_name; child->m_relativePath = child->m_name;
child->m_fullPath = child->m_name; child->m_fullPath = m_fullPath / child->m_name;
AssetBrowserEntry::UpdateChildPaths(child); AssetBrowserEntry::UpdateChildPaths(child);
} }

@ -8,6 +8,7 @@
#include <AzCore/std/string/string.h> #include <AzCore/std/string/string.h>
#include <AzCore/Asset/AssetCommon.h> #include <AzCore/Asset/AssetCommon.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Math/Uuid.h> #include <AzCore/Math/Uuid.h>
#include <AzToolsFramework/AssetBrowser/Entries/AssetBrowserEntry.h> #include <AzToolsFramework/AssetBrowser/Entries/AssetBrowserEntry.h>
@ -77,12 +78,15 @@ namespace AzToolsFramework
private: private:
AZ_DISABLE_COPY_MOVE(RootAssetBrowserEntry); AZ_DISABLE_COPY_MOVE(RootAssetBrowserEntry);
AZStd::string m_enginePath; AZ::IO::Path m_enginePath;
//! Create folder entry child //! Create folder entry child
FolderAssetBrowserEntry* CreateFolder(const char* folderName, AssetBrowserEntry* parent); FolderAssetBrowserEntry* CreateFolder(AZStd::string_view folderName, AssetBrowserEntry* parent);
//! Recursively create folder structure leading to relative path from parent //! Recursively create folder structure leading to path from parent
AssetBrowserEntry* CreateFolders(const char* relativePath, AssetBrowserEntry* parent); AssetBrowserEntry* CreateFolders(AZStd::string_view absolutePath, AssetBrowserEntry* parent);
// Retrieves the nearest ancestor AssetBrowserEntry from the absolutePath
static AssetBrowserEntry* GetNearestAncestor(AZ::IO::PathView absolutePath, AssetBrowserEntry* parent,
AZStd::unordered_set<AssetBrowserEntry*>& visitedSet);
bool m_isInitialUpdate = false; bool m_isInitialUpdate = false;
}; };

@ -26,9 +26,7 @@ namespace AzToolsFramework
if (EntryCache* cache = EntryCache::GetInstance()) if (EntryCache* cache = EntryCache::GetInstance())
{ {
cache->m_fileIdMap.erase(m_fileId); cache->m_fileIdMap.erase(m_fileId);
AZStd::string fullPath = m_fullPath; cache->m_absolutePathToFileId.erase(m_fullPath.LexicallyNormal().Native());
AzFramework::StringFunc::Path::Normalize(fullPath);
cache->m_absolutePathToFileId.erase(fullPath);
if (m_sourceId != -1) if (m_sourceId != -1)
{ {

@ -40,9 +40,9 @@ foreach(project_name project_path IN ZIP_LISTS LY_PROJECTS_TARGET_NAME LY_PROJEC
add_custom_target(${project_name}.Assets add_custom_target(${project_name}.Assets
COMMENT "Processing ${project_name} assets..." COMMENT "Processing ${project_name} assets..."
COMMAND "${CMAKE_COMMAND}" COMMAND "${CMAKE_COMMAND}"
-DLY_LOCK_FILE=$<TARGET_FILE_DIR:AZ::AssetProcessorBatch>/project_assets.lock -DLY_LOCK_FILE=$<GENEX_EVAL:$<TARGET_FILE_DIR:AZ::AssetProcessorBatch>>/project_assets.lock
-P ${LY_ROOT_FOLDER}/cmake/CommandExecution.cmake -P ${LY_ROOT_FOLDER}/cmake/CommandExecution.cmake
EXEC_COMMAND $<TARGET_FILE:AZ::AssetProcessorBatch> EXEC_COMMAND $<GENEX_EVAL:$<TARGET_FILE:AZ::AssetProcessorBatch>>
--zeroAnalysisMode --zeroAnalysisMode
--project-path=${project_real_path} --project-path=${project_real_path}
--platforms=${LY_ASSET_DEPLOY_ASSET_TYPE} --platforms=${LY_ASSET_DEPLOY_ASSET_TYPE}

@ -38,8 +38,19 @@ ly_add_target(
string(REPLACE "." ";" version_list "${LY_VERSION_STRING}") string(REPLACE "." ";" version_list "${LY_VERSION_STRING}")
list(GET version_list 0 EXE_VERSION_INFO_0) list(GET version_list 0 EXE_VERSION_INFO_0)
list(GET version_list 1 EXE_VERSION_INFO_1) list(GET version_list 1 EXE_VERSION_INFO_1)
list(GET version_list 2 EXE_VERSION_INFO_2)
list(GET version_list 3 EXE_VERSION_INFO_3) list(LENGTH version_list version_component_count)
if(${version_component_count} GREATER_EQUAL 3)
list(GET version_list 2 EXE_VERSION_INFO_2)
else()
set(EXE_VERSION_INFO_2 0)
endif()
if(${version_component_count} GREATER_EQUAL 4)
list(GET version_list 3 EXE_VERSION_INFO_3)
else()
set(EXE_VERSION_INFO_3 0)
endif()
ly_add_source_properties( ly_add_source_properties(
SOURCES Shared/CrashHandler.cpp SOURCES Shared/CrashHandler.cpp

@ -8,4 +8,6 @@
set(FILES set(FILES
Python_linux.cpp Python_linux.cpp
ProjectBuilderWorker_linux.cpp ProjectBuilderWorker_linux.cpp
ProjectUtils_linux.cpp
ProjectManagerDefs_linux.cpp
) )

@ -0,0 +1,15 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <ProjectManagerDefs.h>
namespace O3DE::ProjectManager
{
const QString ProjectBuildPathPostfix = ProjectBuildDirectoryName + "/linux";
} // namespace O3DE::ProjectManager

@ -0,0 +1,21 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <ProjectUtils.h>
namespace O3DE::ProjectManager
{
namespace ProjectUtils
{
AZ::Outcome<void, QString> FindSupportedCompilerForPlatform()
{
// Compiler detection not supported on platform
return AZ::Success();
}
} // namespace ProjectUtils
} // namespace O3DE::ProjectManager

@ -8,4 +8,6 @@
set(FILES set(FILES
Python_mac.cpp Python_mac.cpp
ProjectBuilderWorker_mac.cpp ProjectBuilderWorker_mac.cpp
ProjectUtils_mac.cpp
ProjectManagerDefs_mac.cpp
) )

@ -0,0 +1,15 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <ProjectManagerDefs.h>
namespace O3DE::ProjectManager
{
const QString ProjectBuildPathPostfix = ProjectBuildDirectoryName + "/mac_xcode";
} // namespace O3DE::ProjectManager

@ -0,0 +1,21 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <ProjectUtils.h>
namespace O3DE::ProjectManager
{
namespace ProjectUtils
{
AZ::Outcome<void, QString> FindSupportedCompilerForPlatform()
{
// Compiler detection not supported on platform
return AZ::Success();
}
} // namespace ProjectUtils
} // namespace O3DE::ProjectManager

@ -8,4 +8,6 @@
set(FILES set(FILES
Python_windows.cpp Python_windows.cpp
ProjectBuilderWorker_windows.cpp ProjectBuilderWorker_windows.cpp
ProjectUtils_windows.cpp
ProjectManagerDefs_windows.cpp
) )

@ -75,8 +75,17 @@ namespace O3DE::ProjectManager
m_configProjectProcess->start( m_configProjectProcess->start(
"cmake", "cmake",
QStringList{ "-B", QDir(m_projectInfo.m_path).filePath(ProjectBuildPathPostfix), "-S", m_projectInfo.m_path, "-G", QStringList
"Visual Studio 16", "-DLY_3RDPARTY_PATH=" + engineInfo.m_thirdPartyPath }); {
"-B",
QDir(m_projectInfo.m_path).filePath(ProjectBuildPathPostfix),
"-S",
m_projectInfo.m_path,
"-G",
"Visual Studio 16",
"-DLY_3RDPARTY_PATH=" + engineInfo.m_thirdPartyPath,
"-DLY_UNITY_BUILD=1"
});
if (!m_configProjectProcess->waitForStarted()) if (!m_configProjectProcess->waitForStarted())
{ {
@ -124,8 +133,16 @@ namespace O3DE::ProjectManager
m_buildProjectProcess->start( m_buildProjectProcess->start(
"cmake", "cmake",
QStringList{ "--build", QDir(m_projectInfo.m_path).filePath(ProjectBuildPathPostfix), "--target", QStringList
m_projectInfo.m_projectName + ".GameLauncher", "Editor", "--config", "profile" }); {
"--build",
QDir(m_projectInfo.m_path).filePath(ProjectBuildPathPostfix),
"--target",
m_projectInfo.m_projectName + ".GameLauncher",
"Editor",
"--config",
"profile"
});
if (!m_buildProjectProcess->waitForStarted()) if (!m_buildProjectProcess->waitForStarted())
{ {

@ -0,0 +1,14 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <ProjectManagerDefs.h>
namespace O3DE::ProjectManager
{
const QString ProjectBuildPathPostfix = ProjectBuildDirectoryName + "/windows_vs2019";
} // namespace O3DE::ProjectManager

@ -0,0 +1,60 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <ProjectUtils.h>
#include <QDir>
#include <QFileInfo>
#include <QProcess>
#include <QProcessEnvironment>
namespace O3DE::ProjectManager
{
namespace ProjectUtils
{
AZ::Outcome<void, QString> FindSupportedCompilerForPlatform()
{
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
QString programFilesPath = environment.value("ProgramFiles(x86)");
QString vsWherePath = QDir(programFilesPath).filePath("Microsoft Visual Studio/Installer/vswhere.exe");
QFileInfo vsWhereFile(vsWherePath);
if (vsWhereFile.exists() && vsWhereFile.isFile())
{
QProcess vsWhereProcess;
vsWhereProcess.setProcessChannelMode(QProcess::MergedChannels);
vsWhereProcess.start(
vsWherePath,
QStringList{
"-version",
"16.0",
"-latest",
"-requires",
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
"-property",
"isComplete"
});
if (vsWhereProcess.waitForStarted() && vsWhereProcess.waitForFinished())
{
QString vsWhereOutput(vsWhereProcess.readAllStandardOutput());
if (vsWhereOutput.startsWith("1"))
{
return AZ::Success();
}
}
}
return AZ::Failure(QObject::tr("Visual Studio 2019 not found.\n\n"
"Visual Studio 2019 is required to build this project."
" Install any edition of <a href='https://visualstudio.microsoft.com/downloads/'>Visual Studio 2019</a>"
" before proceeding to the next step."));
}
} // namespace ProjectUtils
} // namespace O3DE::ProjectManager

@ -12,6 +12,7 @@
#include <ScreenHeaderWidget.h> #include <ScreenHeaderWidget.h>
#include <GemCatalog/GemModel.h> #include <GemCatalog/GemModel.h>
#include <GemCatalog/GemCatalogScreen.h> #include <GemCatalog/GemCatalogScreen.h>
#include <ProjectUtils.h>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QHBoxLayout> #include <QHBoxLayout>
@ -222,38 +223,42 @@ namespace O3DE::ProjectManager
void CreateProjectCtrl::CreateProject() void CreateProjectCtrl::CreateProject()
{ {
if (m_newProjectSettingsScreen->Validate()) if (ProjectUtils::FindSupportedCompiler(this))
{ {
ProjectInfo projectInfo = m_newProjectSettingsScreen->GetProjectInfo(); if (m_newProjectSettingsScreen->Validate())
QString projectTemplatePath = m_newProjectSettingsScreen->GetProjectTemplatePath();
auto result = PythonBindingsInterface::Get()->CreateProject(projectTemplatePath, projectInfo);
if (result.IsSuccess())
{ {
// automatically register the project ProjectInfo projectInfo = m_newProjectSettingsScreen->GetProjectInfo();
PythonBindingsInterface::Get()->AddProject(projectInfo.m_path); QString projectTemplatePath = m_newProjectSettingsScreen->GetProjectTemplatePath();
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED auto result = PythonBindingsInterface::Get()->CreateProject(projectTemplatePath, projectInfo);
if (!m_gemCatalogScreen->EnableDisableGemsForProject(projectInfo.m_path)) if (result.IsSuccess())
{ {
QMessageBox::critical(this, tr("Failed to configure gems"), tr("Failed to configure gems for template.")); // automatically register the project
return; PythonBindingsInterface::Get()->AddProject(projectInfo.m_path);
}
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
if (!m_gemCatalogScreen->EnableDisableGemsForProject(projectInfo.m_path))
{
QMessageBox::critical(this, tr("Failed to configure gems"), tr("Failed to configure gems for template."));
return;
}
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED #endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
projectInfo.m_needsBuild = true; projectInfo.m_needsBuild = true;
emit NotifyBuildProject(projectInfo); emit NotifyBuildProject(projectInfo);
emit ChangeScreenRequest(ProjectManagerScreen::Projects); emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
else
{
QMessageBox::critical(this, tr("Project creation failed"), tr("Failed to create project."));
}
} }
else else
{ {
QMessageBox::critical(this, tr("Project creation failed"), tr("Failed to create project.")); QMessageBox::warning(
this, tr("Invalid project settings"), tr("Please correct the indicated project settings and try again."));
} }
} }
else
{
QMessageBox::warning(this, tr("Invalid project settings"), tr("Please correct the indicated project settings and try again."));
}
} }
void CreateProjectCtrl::ReinitGemCatalogForSelectedTemplate() void CreateProjectCtrl::ReinitGemCatalogForSelectedTemplate()

@ -70,8 +70,9 @@ namespace O3DE::ProjectManager
m_lastProgress = progress; m_lastProgress = progress;
if (m_projectButton) if (m_projectButton)
{ {
m_projectButton->SetButtonOverlayText(QString("%1 (%2%)\n\n").arg(tr("Building Project..."), QString::number(progress))); m_projectButton->SetButtonOverlayText(QString("%1 (%2%)<br>%3<br>").arg(tr("Building Project..."), QString::number(progress), tr("Click to <a href=\"logs\">view logs</a>.")));
m_projectButton->SetProgressBarValue(progress); m_projectButton->SetProgressBarValue(progress);
m_projectButton->SetBuildLogsLink(m_worker->GetLogFilePath());
} }
} }

@ -14,8 +14,6 @@
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
const QString ProjectBuilderWorker::BuildCancelled = QObject::tr("Build Cancelled.");
ProjectBuilderWorker::ProjectBuilderWorker(const ProjectInfo& projectInfo) ProjectBuilderWorker::ProjectBuilderWorker(const ProjectInfo& projectInfo)
: QObject() : QObject()
, m_projectInfo(projectInfo) , m_projectInfo(projectInfo)

@ -22,7 +22,7 @@ namespace O3DE::ProjectManager
// QProcess::waitForFinished uses -1 to indicate that the process should not timeout // QProcess::waitForFinished uses -1 to indicate that the process should not timeout
static constexpr int MaxBuildTimeMSecs = -1; static constexpr int MaxBuildTimeMSecs = -1;
// Build was cancelled // Build was cancelled
static const QString BuildCancelled; inline static const QString BuildCancelled = QObject::tr("Build Cancelled.");
Q_OBJECT Q_OBJECT

@ -39,7 +39,9 @@ namespace O3DE::ProjectManager
m_overlayLabel->setObjectName("labelButtonOverlay"); m_overlayLabel->setObjectName("labelButtonOverlay");
m_overlayLabel->setWordWrap(true); m_overlayLabel->setWordWrap(true);
m_overlayLabel->setAlignment(Qt::AlignCenter); m_overlayLabel->setAlignment(Qt::AlignCenter);
m_overlayLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
m_overlayLabel->setVisible(false); m_overlayLabel->setVisible(false);
connect(m_overlayLabel, &QLabel::linkActivated, this, &LabelButton::OnLinkActivated);
vLayout->addWidget(m_overlayLabel); vLayout->addWidget(m_overlayLabel);
m_buildOverlayLayout = new QVBoxLayout(); m_buildOverlayLayout = new QVBoxLayout();
@ -231,7 +233,7 @@ namespace O3DE::ProjectManager
AzQtComponents::ShowFileOnDesktop(m_projectInfo.m_path); AzQtComponents::ShowFileOnDesktop(m_projectInfo.m_path);
}); });
menu->addSeparator(); menu->addSeparator();
menu->addAction(tr("Duplicate"), this, [this]() { emit CopyProject(m_projectInfo.m_path); }); menu->addAction(tr("Duplicate"), this, [this]() { emit CopyProject(m_projectInfo); });
menu->addSeparator(); menu->addSeparator();
menu->addAction(tr("Remove from O3DE"), this, [this]() { emit RemoveProject(m_projectInfo.m_path); }); menu->addAction(tr("Remove from O3DE"), this, [this]() { emit RemoveProject(m_projectInfo.m_path); });
menu->addAction(tr("Delete this Project"), this, [this]() { emit DeleteProject(m_projectInfo.m_path); }); menu->addAction(tr("Delete this Project"), this, [this]() { emit DeleteProject(m_projectInfo.m_path); });
@ -266,6 +268,11 @@ namespace O3DE::ProjectManager
SetProjectButtonAction(tr("Build Project"), [this]() { emit BuildProject(m_projectInfo); }); SetProjectButtonAction(tr("Build Project"), [this]() { emit BuildProject(m_projectInfo); });
} }
void ProjectButton::SetBuildLogsLink(const QUrl& logUrl)
{
m_projectImageLabel->SetLogUrl(logUrl);
}
void ProjectButton::ShowBuildFailed(bool show, const QUrl& logUrl) void ProjectButton::ShowBuildFailed(bool show, const QUrl& logUrl)
{ {
if (!logUrl.isEmpty()) if (!logUrl.isEmpty())

@ -77,6 +77,7 @@ namespace O3DE::ProjectManager
void SetProjectButtonAction(const QString& text, AZStd::function<void()> lambda); void SetProjectButtonAction(const QString& text, AZStd::function<void()> lambda);
void SetProjectBuildButtonAction(); void SetProjectBuildButtonAction();
void SetBuildLogsLink(const QUrl& logUrl);
void ShowBuildFailed(bool show, const QUrl& logUrl); void ShowBuildFailed(bool show, const QUrl& logUrl);
void SetLaunchButtonEnabled(bool enabled); void SetLaunchButtonEnabled(bool enabled);
@ -87,7 +88,7 @@ namespace O3DE::ProjectManager
signals: signals:
void OpenProject(const QString& projectName); void OpenProject(const QString& projectName);
void EditProject(const QString& projectName); void EditProject(const QString& projectName);
void CopyProject(const QString& projectName); void CopyProject(const ProjectInfo& projectInfo);
void RemoveProject(const QString& projectName); void RemoveProject(const QString& projectName);
void DeleteProject(const QString& projectName); void DeleteProject(const QString& projectName);
void BuildProject(const ProjectInfo& projectInfo); void BuildProject(const ProjectInfo& projectInfo);

@ -14,8 +14,10 @@ namespace O3DE::ProjectManager
inline constexpr static int ProjectPreviewImageHeight = 280; inline constexpr static int ProjectPreviewImageHeight = 280;
inline constexpr static int ProjectTemplateImageWidth = 92; inline constexpr static int ProjectTemplateImageWidth = 92;
static const QString ProjectBuildPathPostfix = "build/windows_vs2019"; static const QString ProjectBuildDirectoryName = "build";
extern const QString ProjectBuildPathPostfix;
static const QString ProjectBuildPathCmakeFiles = "CMakeFiles"; static const QString ProjectBuildPathCmakeFiles = "CMakeFiles";
static const QString ProjectBuildErrorLogName = "CMakeProjectBuildError.log"; static const QString ProjectBuildErrorLogName = "CMakeProjectBuildError.log";
static const QString ProjectCacheDirectoryName = "Cache";
static const QString ProjectPreviewImagePath = "preview.png"; static const QString ProjectPreviewImagePath = "preview.png";
} // namespace O3DE::ProjectManager } // namespace O3DE::ProjectManager

@ -6,6 +6,7 @@
*/ */
#include <ProjectUtils.h> #include <ProjectUtils.h>
#include <ProjectManagerDefs.h>
#include <PythonBindingsInterface.h> #include <PythonBindingsInterface.h>
#include <QFileDialog> #include <QFileDialog>
@ -17,6 +18,8 @@
#include <QProcessEnvironment> #include <QProcessEnvironment>
#include <QGuiApplication> #include <QGuiApplication>
#include <QProgressDialog> #include <QProgressDialog>
#include <QSpacerItem>
#include <QGridLayout>
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
@ -57,29 +60,63 @@ namespace O3DE::ProjectManager
return false; return false;
} }
static bool SkipFilePaths(const QString& curPath, QStringList& skippedPaths, QStringList& deeperSkippedPaths)
{
bool skip = false;
for (const QString& skippedPath : skippedPaths)
{
QString nativeSkippedPath = QDir::toNativeSeparators(skippedPath);
QString firstSectionSkippedPath = nativeSkippedPath.section(QDir::separator(), 0, 0);
if (curPath == firstSectionSkippedPath)
{
// We are at the end of the path to skip, so skip it
if (nativeSkippedPath == firstSectionSkippedPath)
{
skippedPaths.removeAll(skippedPath);
skip = true;
break;
}
// Append the next section of the skipped path
else
{
deeperSkippedPaths.append(nativeSkippedPath.section(QDir::separator(), 1));
}
}
}
return skip;
}
typedef AZStd::function<void(/*fileCount=*/int, /*totalSizeInBytes=*/int)> StatusFunction; typedef AZStd::function<void(/*fileCount=*/int, /*totalSizeInBytes=*/int)> StatusFunction;
static void RecursiveGetAllFiles(const QDir& directory, QStringList& outFileList, qint64& outTotalSizeInBytes, StatusFunction statusCallback) static void RecursiveGetAllFiles(const QDir& directory, QStringList& skippedPaths, int& outFileCount, qint64& outTotalSizeInBytes, StatusFunction statusCallback)
{ {
const QStringList entries = directory.entryList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot); const QStringList entries = directory.entryList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot);
for (const QString& entryPath : entries) for (const QString& entryPath : entries)
{ {
const QString filePath = QDir::toNativeSeparators(QString("%1/%2").arg(directory.path()).arg(entryPath)); const QString filePath = QDir::toNativeSeparators(QString("%1/%2").arg(directory.path()).arg(entryPath));
QStringList deeperSkippedPaths;
if (SkipFilePaths(entryPath, skippedPaths, deeperSkippedPaths))
{
continue;
}
QFileInfo fileInfo(filePath); QFileInfo fileInfo(filePath);
if (fileInfo.isDir()) if (fileInfo.isDir())
{ {
QDir subDirectory(filePath); QDir subDirectory(filePath);
RecursiveGetAllFiles(subDirectory, outFileList, outTotalSizeInBytes, statusCallback); RecursiveGetAllFiles(subDirectory, deeperSkippedPaths, outFileCount, outTotalSizeInBytes, statusCallback);
} }
else else
{ {
outFileList.push_back(filePath); ++outFileCount;
outTotalSizeInBytes += fileInfo.size(); outTotalSizeInBytes += fileInfo.size();
const int updateStatusEvery = 64; const int updateStatusEvery = 64;
if (outFileList.size() % updateStatusEvery == 0) if (outFileCount % updateStatusEvery == 0)
{ {
statusCallback(outFileList.size(), outTotalSizeInBytes); statusCallback(outFileCount, outTotalSizeInBytes);
} }
} }
} }
@ -88,7 +125,8 @@ namespace O3DE::ProjectManager
static bool CopyDirectory(QProgressDialog* progressDialog, static bool CopyDirectory(QProgressDialog* progressDialog,
const QString& origPath, const QString& origPath,
const QString& newPath, const QString& newPath,
QStringList& filesToCopy, QStringList& skippedPaths,
int filesToCopyCount,
int& outNumCopiedFiles, int& outNumCopiedFiles,
qint64 totalSizeToCopy, qint64 totalSizeToCopy,
qint64& outCopiedFileSize, qint64& outCopiedFileSize,
@ -100,18 +138,24 @@ namespace O3DE::ProjectManager
return false; return false;
} }
for (QString directory : original.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) for (const QString& directory : original.entryList(QDir::Dirs | QDir::NoDotAndDotDot))
{ {
if (progressDialog->wasCanceled()) if (progressDialog->wasCanceled())
{ {
return false; return false;
} }
QStringList deeperSkippedPaths;
if (SkipFilePaths(directory, skippedPaths, deeperSkippedPaths))
{
continue;
}
QString newDirectoryPath = newPath + QDir::separator() + directory; QString newDirectoryPath = newPath + QDir::separator() + directory;
original.mkpath(newDirectoryPath); original.mkpath(newDirectoryPath);
if (!CopyDirectory(progressDialog, origPath + QDir::separator() + directory, if (!CopyDirectory(progressDialog, origPath + QDir::separator() + directory, newDirectoryPath, deeperSkippedPaths,
newDirectoryPath, filesToCopy, outNumCopiedFiles, totalSizeToCopy, outCopiedFileSize, showIgnoreFileDialog)) filesToCopyCount, outNumCopiedFiles, totalSizeToCopy, outCopiedFileSize, showIgnoreFileDialog))
{ {
return false; return false;
} }
@ -119,18 +163,25 @@ namespace O3DE::ProjectManager
QLocale locale; QLocale locale;
const float progressDialogRangeHalf = qFabs(progressDialog->maximum() - progressDialog->minimum()) * 0.5f; const float progressDialogRangeHalf = qFabs(progressDialog->maximum() - progressDialog->minimum()) * 0.5f;
for (QString file : original.entryList(QDir::Files)) for (const QString& file : original.entryList(QDir::Files))
{ {
if (progressDialog->wasCanceled()) if (progressDialog->wasCanceled())
{ {
return false; return false;
} }
// Unused by this function but neccesary to pass in to SkipFilePaths
QStringList deeperSkippedPaths;
if (SkipFilePaths(file, skippedPaths, deeperSkippedPaths))
{
continue;
}
// Progress window update // Progress window update
{ {
// Weight in the number of already copied files as well as the copied bytes to get a better progress indication // Weight in the number of already copied files as well as the copied bytes to get a better progress indication
// for cases combining many small files and some really large files. // for cases combining many small files and some really large files.
const float normalizedNumFiles = static_cast<float>(outNumCopiedFiles) / filesToCopy.count(); const float normalizedNumFiles = static_cast<float>(outNumCopiedFiles) / filesToCopyCount;
const float normalizedFileSize = static_cast<float>(outCopiedFileSize) / totalSizeToCopy; const float normalizedFileSize = static_cast<float>(outCopiedFileSize) / totalSizeToCopy;
const int progress = normalizedNumFiles * progressDialogRangeHalf + normalizedFileSize * progressDialogRangeHalf; const int progress = normalizedNumFiles * progressDialogRangeHalf + normalizedFileSize * progressDialogRangeHalf;
progressDialog->setValue(progress); progressDialog->setValue(progress);
@ -138,7 +189,7 @@ namespace O3DE::ProjectManager
const QString copiedFileSizeString = locale.formattedDataSize(outCopiedFileSize); const QString copiedFileSizeString = locale.formattedDataSize(outCopiedFileSize);
const QString totalFileSizeString = locale.formattedDataSize(totalSizeToCopy); const QString totalFileSizeString = locale.formattedDataSize(totalSizeToCopy);
progressDialog->setLabelText(QString("Coping file %1 of %2 (%3 of %4) ...").arg(QString::number(outNumCopiedFiles), progressDialog->setLabelText(QString("Coping file %1 of %2 (%3 of %4) ...").arg(QString::number(outNumCopiedFiles),
QString::number(filesToCopy.count()), QString::number(filesToCopyCount),
copiedFileSizeString, copiedFileSizeString,
totalFileSizeString)); totalFileSizeString));
qApp->processEvents(QEventLoop::ExcludeUserInputEvents); qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
@ -192,6 +243,39 @@ namespace O3DE::ProjectManager
return true; return true;
} }
static bool ClearProjectBuildArtifactsAndCache(const QString& origPath, const QString& newPath, QWidget* parent)
{
QDir buildDirectory = QDir(newPath);
if ((!buildDirectory.cd(ProjectBuildDirectoryName) || !DeleteProjectFiles(buildDirectory.path(), true))
&& QDir(origPath).cd(ProjectBuildDirectoryName))
{
QMessageBox::warning(
parent,
QObject::tr("Clear Build Artifacts"),
QObject::tr("Build artifacts failed to delete for moved project. Please manually delete build directory at \"%1\"")
.arg(buildDirectory.path()),
QMessageBox::Close);
return false;
}
QDir cacheDirectory = QDir(newPath);
if ((!cacheDirectory.cd(ProjectCacheDirectoryName) || !DeleteProjectFiles(cacheDirectory.path(), true))
&& QDir(origPath).cd(ProjectCacheDirectoryName))
{
QMessageBox::warning(
parent,
QObject::tr("Clear Asset Cache"),
QObject::tr("Asset cache failed to delete for moved project. Please manually delete cache directory at \"%1\"")
.arg(cacheDirectory.path()),
QMessageBox::Close);
return false;
}
return false;
}
bool AddProjectDialog(QWidget* parent) bool AddProjectDialog(QWidget* parent)
{ {
QString path = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(parent, QObject::tr("Select Project Directory"))); QString path = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(parent, QObject::tr("Select Project Directory")));
@ -213,7 +297,7 @@ namespace O3DE::ProjectManager
return PythonBindingsInterface::Get()->RemoveProject(path); return PythonBindingsInterface::Get()->RemoveProject(path);
} }
bool CopyProjectDialog(const QString& origPath, QWidget* parent) bool CopyProjectDialog(const QString& origPath, ProjectInfo& newProjectInfo, QWidget* parent)
{ {
bool copyResult = false; bool copyResult = false;
@ -223,6 +307,8 @@ namespace O3DE::ProjectManager
QFileDialog::getExistingDirectory(parent, QObject::tr("Select New Project Directory"), parentOrigDir.path())); QFileDialog::getExistingDirectory(parent, QObject::tr("Select New Project Directory"), parentOrigDir.path()));
if (!newPath.isEmpty()) if (!newPath.isEmpty())
{ {
newProjectInfo.m_path = newPath;
if (!WarnDirectoryOverwrite(newPath, parent)) if (!WarnDirectoryOverwrite(newPath, parent))
{ {
return false; return false;
@ -234,7 +320,7 @@ namespace O3DE::ProjectManager
return copyResult; return copyResult;
} }
bool CopyProject(const QString& origPath, const QString& newPath, QWidget* parent) bool CopyProject(const QString& origPath, const QString& newPath, QWidget* parent, bool skipRegister)
{ {
// Disallow copying from or into subdirectory // Disallow copying from or into subdirectory
if (IsDirectoryDescedent(origPath, newPath) || IsDirectoryDescedent(newPath, origPath)) if (IsDirectoryDescedent(origPath, newPath) || IsDirectoryDescedent(newPath, origPath))
@ -242,8 +328,13 @@ namespace O3DE::ProjectManager
return false; return false;
} }
QStringList filesToCopy; int filesToCopyCount = 0;
qint64 totalSizeInBytes = 0; qint64 totalSizeInBytes = 0;
QStringList skippedPaths
{
ProjectBuildDirectoryName,
ProjectCacheDirectoryName
};
QProgressDialog* progressDialog = new QProgressDialog(parent); QProgressDialog* progressDialog = new QProgressDialog(parent);
progressDialog->setAutoClose(true); progressDialog->setAutoClose(true);
@ -254,7 +345,8 @@ namespace O3DE::ProjectManager
progressDialog->show(); progressDialog->show();
QLocale locale; QLocale locale;
RecursiveGetAllFiles(origPath, filesToCopy, totalSizeInBytes, [=](int fileCount, int sizeInBytes) QStringList getFilesSkippedPaths(skippedPaths);
RecursiveGetAllFiles(origPath, getFilesSkippedPaths, filesToCopyCount, totalSizeInBytes, [=](int fileCount, int sizeInBytes)
{ {
// Create a human-readable version of the file size. // Create a human-readable version of the file size.
const QString fileSizeString = locale.formattedDataSize(sizeInBytes); const QString fileSizeString = locale.formattedDataSize(sizeInBytes);
@ -273,8 +365,10 @@ namespace O3DE::ProjectManager
// Phase 1: Copy files // Phase 1: Copy files
bool showIgnoreFileDialog = true; bool showIgnoreFileDialog = true;
bool success = CopyDirectory(progressDialog, origPath, newPath, filesToCopy, numFilesCopied, totalSizeInBytes, copiedFileSize, showIgnoreFileDialog); QStringList copyFilesSkippedPaths(skippedPaths);
if (success) bool success = CopyDirectory(progressDialog, origPath, newPath, copyFilesSkippedPaths, filesToCopyCount, numFilesCopied,
totalSizeInBytes, copiedFileSize, showIgnoreFileDialog);
if (success && !skipRegister)
{ {
// Phase 2: Register project // Phase 2: Register project
success = RegisterProject(newPath); success = RegisterProject(newPath);
@ -297,7 +391,7 @@ namespace O3DE::ProjectManager
QDir projectDirectory(path); QDir projectDirectory(path);
if (projectDirectory.exists()) if (projectDirectory.exists())
{ {
// Check if there is an actual project hereor just force it // Check if there is an actual project here or just force it
if (force || PythonBindingsInterface::Get()->GetProject(path).IsSuccess()) if (force || PythonBindingsInterface::Get()->GetProject(path).IsSuccess())
{ {
return projectDirectory.removeRecursively(); return projectDirectory.removeRecursively();
@ -307,12 +401,12 @@ namespace O3DE::ProjectManager
return false; return false;
} }
bool MoveProject(QString origPath, QString newPath, QWidget* parent, bool ignoreRegister) bool MoveProject(QString origPath, QString newPath, QWidget* parent, bool skipRegister)
{ {
origPath = QDir::toNativeSeparators(origPath); origPath = QDir::toNativeSeparators(origPath);
newPath = QDir::toNativeSeparators(newPath); newPath = QDir::toNativeSeparators(newPath);
if (!WarnDirectoryOverwrite(newPath, parent) || (!ignoreRegister && !UnregisterProject(origPath))) if (!WarnDirectoryOverwrite(newPath, parent) || (!skipRegister && !UnregisterProject(origPath)))
{ {
return false; return false;
} }
@ -332,8 +426,13 @@ namespace O3DE::ProjectManager
DeleteProjectFiles(origPath, true); DeleteProjectFiles(origPath, true);
} }
else
{
// If directoy rename succeeded then build and cache directories need to be deleted seperately
ClearProjectBuildArtifactsAndCache(origPath, newPath, parent);
}
if (!ignoreRegister && !RegisterProject(newPath)) if (!skipRegister && !RegisterProject(newPath))
{ {
return false; return false;
} }
@ -374,46 +473,27 @@ namespace O3DE::ProjectManager
return true; return true;
} }
static bool IsVS2019Installed_internal() bool FindSupportedCompiler(QWidget* parent)
{ {
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); auto findCompilerResult = FindSupportedCompilerForPlatform();
QString programFilesPath = environment.value("ProgramFiles(x86)");
QString vsWherePath = programFilesPath + "\\Microsoft Visual Studio\\Installer\\vswhere.exe";
QFileInfo vsWhereFile(vsWherePath); if (!findCompilerResult.IsSuccess())
if (vsWhereFile.exists() && vsWhereFile.isFile())
{ {
QProcess vsWhereProcess; QMessageBox vsWarningMessage(parent);
vsWhereProcess.setProcessChannelMode(QProcess::MergedChannels); vsWarningMessage.setIcon(QMessageBox::Warning);
vsWarningMessage.setWindowTitle(QObject::tr("Create Project"));
vsWhereProcess.start( // Makes link clickable
vsWherePath, vsWarningMessage.setTextFormat(Qt::RichText);
QStringList{ "-version", "16.0", "-latest", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", vsWarningMessage.setText(findCompilerResult.GetError());
"-property", "isComplete" }); vsWarningMessage.setStandardButtons(QMessageBox::Close);
if (!vsWhereProcess.waitForStarted()) QSpacerItem* horizontalSpacer = new QSpacerItem(600, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
{ QGridLayout* layout = reinterpret_cast<QGridLayout*>(vsWarningMessage.layout());
return false; layout->addItem(horizontalSpacer, layout->rowCount(), 0, 1, layout->columnCount());
} vsWarningMessage.exec();
while (vsWhereProcess.waitForReadyRead())
{
}
QString vsWhereOutput(vsWhereProcess.readAllStandardOutput());
if (vsWhereOutput.startsWith("1"))
{
return true;
}
} }
return false; return findCompilerResult.IsSuccess();
}
bool IsVS2019Installed()
{
static bool vs2019Installed = IsVS2019Installed_internal();
return vs2019Installed;
} }
ProjectManagerScreen GetProjectManagerScreen(const QString& screen) ProjectManagerScreen GetProjectManagerScreen(const QString& screen)

@ -7,7 +7,10 @@
#pragma once #pragma once
#include <ScreenDefs.h> #include <ScreenDefs.h>
#include <ProjectInfo.h>
#include <QWidget> #include <QWidget>
#include <AzCore/Outcome/Outcome.h>
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
@ -16,14 +19,15 @@ namespace O3DE::ProjectManager
bool AddProjectDialog(QWidget* parent = nullptr); bool AddProjectDialog(QWidget* parent = nullptr);
bool RegisterProject(const QString& path); bool RegisterProject(const QString& path);
bool UnregisterProject(const QString& path); bool UnregisterProject(const QString& path);
bool CopyProjectDialog(const QString& origPath, QWidget* parent = nullptr); bool CopyProjectDialog(const QString& origPath, ProjectInfo& newProjectInfo, QWidget* parent = nullptr);
bool CopyProject(const QString& origPath, const QString& newPath, QWidget* parent); bool CopyProject(const QString& origPath, const QString& newPath, QWidget* parent, bool skipRegister = false);
bool DeleteProjectFiles(const QString& path, bool force = false); bool DeleteProjectFiles(const QString& path, bool force = false);
bool MoveProject(QString origPath, QString newPath, QWidget* parent = nullptr, bool ignoreRegister = false); bool MoveProject(QString origPath, QString newPath, QWidget* parent, bool skipRegister = false);
bool ReplaceFile(const QString& origFile, const QString& newFile, QWidget* parent = nullptr, bool interactive = true); bool ReplaceFile(const QString& origFile, const QString& newFile, QWidget* parent = nullptr, bool interactive = true);
bool IsVS2019Installed(); bool FindSupportedCompiler(QWidget* parent = nullptr);
AZ::Outcome<void, QString> FindSupportedCompilerForPlatform();
ProjectManagerScreen GetProjectManagerScreen(const QString& screen); ProjectManagerScreen GetProjectManagerScreen(const QString& screen);
} // namespace ProjectUtils } // namespace ProjectUtils

@ -384,14 +384,17 @@ namespace O3DE::ProjectManager
emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject); emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
} }
} }
void ProjectsScreen::HandleCopyProject(const QString& projectPath) void ProjectsScreen::HandleCopyProject(const ProjectInfo& projectInfo)
{ {
if (!WarnIfInBuildQueue(projectPath)) if (!WarnIfInBuildQueue(projectInfo.m_path))
{ {
ProjectInfo newProjectInfo(projectInfo);
// Open file dialog and choose location for copied project then register copy with O3DE // Open file dialog and choose location for copied project then register copy with O3DE
if (ProjectUtils::CopyProjectDialog(projectPath, this)) if (ProjectUtils::CopyProjectDialog(projectInfo.m_path, newProjectInfo, this))
{ {
ResetProjectsContent(); ResetProjectsContent();
emit NotifyBuildProject(newProjectInfo);
emit ChangeScreenRequest(ProjectManagerScreen::Projects); emit ChangeScreenRequest(ProjectManagerScreen::Projects);
} }
} }
@ -516,7 +519,7 @@ namespace O3DE::ProjectManager
bool ProjectsScreen::StartProjectBuild(const ProjectInfo& projectInfo) bool ProjectsScreen::StartProjectBuild(const ProjectInfo& projectInfo)
{ {
if (ProjectUtils::IsVS2019Installed()) if (ProjectUtils::FindSupportedCompiler(this))
{ {
QMessageBox::StandardButton buildProject = QMessageBox::information( QMessageBox::StandardButton buildProject = QMessageBox::information(
this, this,

@ -44,7 +44,7 @@ namespace O3DE::ProjectManager
void HandleAddProjectButton(); void HandleAddProjectButton();
void HandleOpenProject(const QString& projectPath); void HandleOpenProject(const QString& projectPath);
void HandleEditProject(const QString& projectPath); void HandleEditProject(const QString& projectPath);
void HandleCopyProject(const QString& projectPath); void HandleCopyProject(const ProjectInfo& projectInfo);
void HandleRemoveProject(const QString& projectPath); void HandleRemoveProject(const QString& projectPath);
void HandleDeleteProject(const QString& projectPath); void HandleDeleteProject(const QString& projectPath);

@ -219,11 +219,13 @@ namespace O3DE::ProjectManager
// Move project first to avoid trying to update settings at the new location before it has been moved there // Move project first to avoid trying to update settings at the new location before it has been moved there
if (newProjectSettings.m_path != m_projectInfo.m_path) if (newProjectSettings.m_path != m_projectInfo.m_path)
{ {
if (!ProjectUtils::MoveProject(m_projectInfo.m_path, newProjectSettings.m_path)) if (!ProjectUtils::MoveProject(m_projectInfo.m_path, newProjectSettings.m_path, this))
{ {
QMessageBox::critical(this, tr("Project move failed"), tr("Failed to move project.")); QMessageBox::critical(this, tr("Project move failed"), tr("Failed to move project."));
return false; return false;
} }
emit NotifyBuildProject(newProjectSettings);
} }
// Update project if settings changed // Update project if settings changed

@ -8,6 +8,7 @@
#include <AzCore/UnitTest/TestTypes.h> #include <AzCore/UnitTest/TestTypes.h>
#include <Application.h> #include <Application.h>
#include <ProjectUtils.h> #include <ProjectUtils.h>
#include <ProjectManagerDefs.h>
#include <ProjectManager_Test_Traits_Platform.h> #include <ProjectManager_Test_Traits_Platform.h>
#include <QFile> #include <QFile>
@ -25,16 +26,31 @@ namespace O3DE::ProjectManager
: public ::UnitTest::ScopedAllocatorSetupFixture : public ::UnitTest::ScopedAllocatorSetupFixture
{ {
public: public:
static inline QString ReplaceFirstAWithB(const QString& originalString)
{
QString bString(originalString);
return bString.replace(bString.indexOf('A'), 1, 'B');
}
ProjectManagerUtilsTests() ProjectManagerUtilsTests()
{ {
m_application = AZStd::make_unique<ProjectManager::Application>(); m_application = AZStd::make_unique<ProjectManager::Application>();
m_application->Init(false); m_application->Init(false);
m_projectAPath = "ProjectA";
// Replaces first 'A' with 'B'
m_projectBPath = ReplaceFirstAWithB(m_projectAPath);
m_projectABuildPath = QString("%1%2%3").arg(m_projectAPath, QDir::separator(), ProjectBuildDirectoryName);
m_projectBBuildPath = ReplaceFirstAWithB(m_projectABuildPath);
QDir dir; QDir dir;
dir.mkdir("ProjectA"); dir.mkpath(m_projectABuildPath);
dir.mkdir("ProjectB"); dir.mkdir(m_projectBPath);
QFile origFile("ProjectA/origFile.txt"); m_projectAOrigFilePath = QString("%1%2%3").arg(m_projectAPath, QDir::separator(), "origFile.txt");
m_projectBOrigFilePath = ReplaceFirstAWithB(m_projectAOrigFilePath);
QFile origFile(m_projectAOrigFilePath);
if (origFile.open(QIODevice::ReadWrite)) if (origFile.open(QIODevice::ReadWrite))
{ {
QTextStream stream(&origFile); QTextStream stream(&origFile);
@ -42,63 +58,153 @@ namespace O3DE::ProjectManager
origFile.close(); origFile.close();
} }
QFile replaceFile("ProjectA/replaceFile.txt"); m_projectAReplaceFilePath = QString("%1%2%3").arg(m_projectAPath, QDir::separator(), "replaceFile.txt");
m_projectBReplaceFilePath = ReplaceFirstAWithB(m_projectAReplaceFilePath);
QFile replaceFile(m_projectAReplaceFilePath);
if (replaceFile.open(QIODevice::ReadWrite)) if (replaceFile.open(QIODevice::ReadWrite))
{ {
QTextStream stream(&replaceFile); QTextStream stream(&replaceFile);
stream << "replace" << Qt::endl; stream << "replace" << Qt::endl;
replaceFile.close(); replaceFile.close();
} }
m_projectABuildFilePath = QString("%1%2%3").arg(m_projectABuildPath, QDir::separator(), "build.obj");
m_projectBBuildFilePath = ReplaceFirstAWithB(m_projectABuildFilePath);
QFile buildFile(m_projectABuildFilePath);
if (buildFile.open(QIODevice::ReadWrite))
{
QTextStream stream(&buildFile);
stream << "x0FFFFFFFF" << Qt::endl;
buildFile.close();
}
} }
~ProjectManagerUtilsTests() ~ProjectManagerUtilsTests()
{ {
QDir dirA("ProjectA"); QDir dirA(m_projectAPath);
dirA.removeRecursively(); dirA.removeRecursively();
QDir dirB("ProjectB"); QDir dirB(m_projectBPath);
dirB.removeRecursively(); dirB.removeRecursively();
m_application.reset(); m_application.reset();
} }
AZStd::unique_ptr<ProjectManager::Application> m_application; AZStd::unique_ptr<ProjectManager::Application> m_application;
QString m_projectAPath;
QString m_projectAOrigFilePath;
QString m_projectAReplaceFilePath;
QString m_projectABuildPath;
QString m_projectABuildFilePath;
QString m_projectBPath;
QString m_projectBOrigFilePath;
QString m_projectBReplaceFilePath;
QString m_projectBBuildPath;
QString m_projectBBuildFilePath;
}; };
#if AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS #if AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
TEST_F(ProjectManagerUtilsTests, DISABLED_MoveProject_Succeeds) TEST_F(ProjectManagerUtilsTests, DISABLED_MoveProject_MovesExpectedFiles)
#else
TEST_F(ProjectManagerUtilsTests, MoveProject_MovesExpectedFiles)
#endif // !AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
{
EXPECT_TRUE(MoveProject(
QDir::currentPath() + QDir::separator() + m_projectAPath,
QDir::currentPath() + QDir::separator() + m_projectBPath,
nullptr, true));
QFileInfo origFile(m_projectAOrigFilePath);
EXPECT_FALSE(origFile.exists());
QFileInfo replaceFile(m_projectAReplaceFilePath);
EXPECT_FALSE(replaceFile.exists());
QFileInfo origFileMoved(m_projectBOrigFilePath);
EXPECT_TRUE(origFileMoved.exists() && origFileMoved.isFile());
QFileInfo replaceFileMoved(m_projectBReplaceFilePath);
EXPECT_TRUE(replaceFileMoved.exists() && replaceFileMoved.isFile());
}
#if AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
TEST_F(ProjectManagerUtilsTests, DISABLED_MoveProject_DoesntMoveBuild)
#else #else
TEST_F(ProjectManagerUtilsTests, MoveProject_Succeeds) TEST_F(ProjectManagerUtilsTests, MoveProject_DoesntMoveBuild)
#endif // !AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS #endif // !AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
{ {
EXPECT_TRUE(MoveProject( EXPECT_TRUE(MoveProject(
QDir::currentPath() + QDir::separator() + "ProjectA", QDir::currentPath() + QDir::separator() + m_projectAPath,
QDir::currentPath() + QDir::separator() + "ProjectB", QDir::currentPath() + QDir::separator() + m_projectBPath,
nullptr, true)); nullptr, true));
QFileInfo origFile("ProjectA/origFile.txt"); QFileInfo origFile(m_projectAOrigFilePath);
EXPECT_TRUE(!origFile.exists()); EXPECT_FALSE(origFile.exists());
QFileInfo replaceFile("ProjectA/replaceFile.txt"); QFileInfo origFileMoved(m_projectBOrigFilePath);
EXPECT_TRUE(!replaceFile.exists()); EXPECT_TRUE(origFileMoved.exists() && origFileMoved.isFile());
QDir buildDir(m_projectBBuildPath);
EXPECT_FALSE(buildDir.exists());
}
#if AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
TEST_F(ProjectManagerUtilsTests, DISABLED_CopyProject_CopiesExpectedFiles)
#else
TEST_F(ProjectManagerUtilsTests, CopyProject_CopiesExpectedFiles)
#endif // !AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
{
EXPECT_TRUE(CopyProject(
QDir::currentPath() + QDir::separator() + m_projectAPath,
QDir::currentPath() + QDir::separator() + m_projectBPath,
nullptr, true));
QFileInfo origFileMoved("ProjectB/origFile.txt"); QFileInfo origFile(m_projectAOrigFilePath);
EXPECT_TRUE(origFile.exists());
QFileInfo replaceFile(m_projectAReplaceFilePath);
EXPECT_TRUE(replaceFile.exists());
QFileInfo origFileMoved(m_projectBOrigFilePath);
EXPECT_TRUE(origFileMoved.exists() && origFileMoved.isFile()); EXPECT_TRUE(origFileMoved.exists() && origFileMoved.isFile());
QFileInfo replaceFileMoved("ProjectB/replaceFile.txt"); QFileInfo replaceFileMoved(m_projectBReplaceFilePath);
EXPECT_TRUE(replaceFileMoved.exists() && replaceFileMoved.isFile()); EXPECT_TRUE(replaceFileMoved.exists() && replaceFileMoved.isFile());
} }
#if AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
TEST_F(ProjectManagerUtilsTests, DISABLED_CopyProject_DoesntCopyBuild)
#else
TEST_F(ProjectManagerUtilsTests, CopyProject_DoesntCopyBuild)
#endif // !AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
{
EXPECT_TRUE(CopyProject(
QDir::currentPath() + QDir::separator() + m_projectAPath,
QDir::currentPath() + QDir::separator() + m_projectBPath,
nullptr, true));
QFileInfo origFile(m_projectAOrigFilePath);
EXPECT_TRUE(origFile.exists());
QFileInfo origFileMoved(m_projectBOrigFilePath);
EXPECT_TRUE(origFileMoved.exists() && origFileMoved.isFile());
QDir buildDir(m_projectBBuildPath);
EXPECT_FALSE(buildDir.exists());
}
#if AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS #if AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
TEST_F(ProjectManagerUtilsTests, DISABLED_ReplaceFile_Succeeds) TEST_F(ProjectManagerUtilsTests, DISABLED_ReplaceFile_Succeeds)
#else #else
TEST_F(ProjectManagerUtilsTests, ReplaceFile_Succeeds) TEST_F(ProjectManagerUtilsTests, ReplaceFile_Succeeds)
#endif // !AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS #endif // !AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
{ {
EXPECT_TRUE(ReplaceFile("ProjectA/origFile.txt", "ProjectA/replaceFile.txt", nullptr, false)); EXPECT_TRUE(ReplaceFile(m_projectAOrigFilePath, m_projectAReplaceFilePath, nullptr, false));
QFile origFile("ProjectA/origFile.txt"); QFile origFile(m_projectAOrigFilePath);
if (origFile.open(QIODevice::ReadOnly)) EXPECT_TRUE(origFile.open(QIODevice::ReadOnly));
{ {
QTextStream stream(&origFile); QTextStream stream(&origFile);
QString line = stream.readLine(); QString line = stream.readLine();
@ -106,10 +212,6 @@ namespace O3DE::ProjectManager
origFile.close(); origFile.close();
} }
else
{
FAIL();
}
} }
} // namespace ProjectUtils } // namespace ProjectUtils
} // namespace O3DE::ProjectManager } // namespace O3DE::ProjectManager

@ -6,7 +6,7 @@
"type": "Code", "type": "Code",
"summary": "The Asset Memory Analyzer Gem provides tools to profile asset memory usage in Open 3D Engine through ImGUI (Immediate Mode Graphical User Interface).", "summary": "The Asset Memory Analyzer Gem provides tools to profile asset memory usage in Open 3D Engine through ImGUI (Immediate Mode Graphical User Interface).",
"canonical_tags": ["Gem"], "canonical_tags": ["Gem"],
"user_tags": ["Debug", "Utillity", "Tools"], "user_tags": ["Debug", "Utility", "Tools"],
"icon_path": "preview.png", "icon_path": "preview.png",
"requirements": "" "requirements": ""
} }

@ -14,11 +14,3 @@ add_subdirectory(RPI)
add_subdirectory(Tools) add_subdirectory(Tools)
add_subdirectory(Utils) add_subdirectory(Utils)
# The "Atom" Gem will alias the real Atom_AtomBridge target variants
# allows the enabling and disabling the "Atom" Gem to build the pre-requisite dependencies
ly_create_alias(NAME Atom.Clients NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Clients)
ly_create_alias(NAME Atom.Servers NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Servers)
if(PAL_TRAIT_BUILD_HOST_TOOLS)
ly_create_alias(NAME Atom.Builders NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Builders)
ly_create_alias(NAME Atom.Tools NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Tools)
endif()

@ -6,3 +6,7 @@
# #
add_subdirectory(ReferenceMaterials) add_subdirectory(ReferenceMaterials)
add_subdirectory(Sponza) add_subdirectory(Sponza)
if(PAL_TRAIT_BUILD_HOST_TOOLS)
ly_create_alias(NAME AtomContent.Builders NAMESPACE Gem TARGETS Gem::AtomContent_ReferenceMaterials.Builders Gem::AtomContent_Sponza.Builders)
endif()

@ -115,3 +115,18 @@ if(PAL_TRAIT_BUILD_HOST_TOOLS)
ly_create_alias(NAME Atom_AtomBridge.Builders NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Editor) ly_create_alias(NAME Atom_AtomBridge.Builders NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Editor)
ly_create_alias(NAME Atom_AtomBridge.Tools NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Editor) ly_create_alias(NAME Atom_AtomBridge.Tools NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Editor)
endif() endif()
# The "Atom" Gem will alias the real Atom_AtomBridge target variants
# allows the enabling and disabling the "Atom" Gem to build the pre-requisite dependencies
# The "AtomLyIntegration" Gem will also alias the real Atom_AtomBridge target variants
# The Atom Gem does the same at the moment.
ly_create_alias(NAME Atom.Clients NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Clients)
ly_create_alias(NAME Atom.Servers NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Servers)
ly_create_alias(NAME AtomLyIntegration.Clients NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Clients)
ly_create_alias(NAME AtomLyIntegration.Servers NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Servers)
if(PAL_TRAIT_BUILD_HOST_TOOLS)
ly_create_alias(NAME Atom.Builders NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Builders)
ly_create_alias(NAME Atom.Tools NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Tools)
ly_create_alias(NAME AtomLyIntegration.Builders NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Builders)
ly_create_alias(NAME AtomLyIntegration.Tools NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Tools)
endif()

@ -15,11 +15,3 @@ add_subdirectory(AtomBridge)
add_subdirectory(AtomViewportDisplayInfo) add_subdirectory(AtomViewportDisplayInfo)
add_subdirectory(AtomViewportDisplayIcons) add_subdirectory(AtomViewportDisplayIcons)
# The "AtomLyIntegration" Gem will also alias the real Atom_AtomBridge target variants
# The Atom Gem does the same at the moment.
ly_create_alias(NAME AtomLyIntegration.Clients NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Clients)
ly_create_alias(NAME AtomLyIntegration.Servers NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Servers)
if(PAL_TRAIT_BUILD_HOST_TOOLS)
ly_create_alias(NAME AtomLyIntegration.Builders NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Builders)
ly_create_alias(NAME AtomLyIntegration.Tools NAMESPACE Gem TARGETS Gem::Atom_AtomBridge.Tools)
endif()

@ -57,5 +57,7 @@ if(PAL_TRAIT_BUILD_HOST_TOOLS)
PRIVATE PRIVATE
AZ::AzCore AZ::AzCore
Gem::EMotionFX_Atom.Static Gem::EMotionFX_Atom.Static
RUNTIME_DEPENDENCIES
Gem::EMotionFX.Editor
) )
endif() endif()

@ -6,7 +6,7 @@
"type": "Code", "type": "Code",
"summary": "The Wwise Audio Engine Gem provides support for Audiokinetic Wave Works Interactive Sound Engine (Wwise).", "summary": "The Wwise Audio Engine Gem provides support for Audiokinetic Wave Works Interactive Sound Engine (Wwise).",
"canonical_tags": ["Gem"], "canonical_tags": ["Gem"],
"user_tags": ["Audio", "Utiltity", "Tools"], "user_tags": ["Audio", "Utility", "Tools"],
"icon_path": "preview.png", "icon_path": "preview.png",
"requirements": "Users will need to download WWise from the AudioKinetic web site: https://www.audiokinetic.com/download/" "requirements": "Users will need to download WWise from the AudioKinetic web site: https://www.audiokinetic.com/download/"
} }

@ -6,7 +6,7 @@
"type": "Code", "type": "Code",
"summary": "The Audio System Gem provides the Audio Translation Layer (ATL) and Audio Controls Editor, which add support for audio in Open 3D Engine.", "summary": "The Audio System Gem provides the Audio Translation Layer (ATL) and Audio Controls Editor, which add support for audio in Open 3D Engine.",
"canonical_tags": ["Gem"], "canonical_tags": ["Gem"],
"user_tags": ["Audio", "Utiltity", "Tools"], "user_tags": ["Audio", "Utility", "Tools"],
"icon_path": "preview.png", "icon_path": "preview.png",
"requirements": "" "requirements": ""
} }

@ -143,6 +143,7 @@ namespace EMotionFX
Group::ActorGroup* group = azrtti_cast<Group::ActorGroup*>(&target); Group::ActorGroup* group = azrtti_cast<Group::ActorGroup*>(&target);
group->SetName(AZ::SceneAPI::DataTypes::Utilities::CreateUniqueName<Group::IActorGroup>(scene.GetName(), scene.GetManifest())); group->SetName(AZ::SceneAPI::DataTypes::Utilities::CreateUniqueName<Group::IActorGroup>(scene.GetName(), scene.GetManifest()));
group->SetBestMatchingRootBone(scene.GetGraph());
// LOD Rule need to be built first in the actor, so we know which mesh and bone belongs to LOD. // LOD Rule need to be built first in the actor, so we know which mesh and bone belongs to LOD.
// After this call, LOD rule will be populated with all the LOD bones // After this call, LOD rule will be populated with all the LOD bones

@ -9,10 +9,14 @@
#include <AzCore/Memory/SystemAllocator.h> #include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/Serialization/SerializeContext.h> #include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContext.h> #include <AzCore/Serialization/EditContext.h>
#include <SceneAPI/SceneCore/Containers/SceneGraph.h>
#include <SceneAPI/SceneCore/Containers/Views/PairIterator.h>
#include <SceneAPI/SceneCore/Containers/Views/SceneGraphDownwardsIterator.h>
#include <SceneAPI/SceneCore/DataTypes/Rules/IRule.h> #include <SceneAPI/SceneCore/DataTypes/Rules/IRule.h>
#include <SceneAPI/SceneCore/DataTypes/GraphData/IBoneData.h> #include <SceneAPI/SceneCore/DataTypes/GraphData/IBoneData.h>
#include <SceneAPI/SceneCore/DataTypes/GraphData/IMeshData.h> #include <SceneAPI/SceneCore/DataTypes/GraphData/IMeshData.h>
#include <SceneAPI/SceneCore/DataTypes/Groups/ISceneNodeGroup.h> #include <SceneAPI/SceneCore/DataTypes/Groups/ISceneNodeGroup.h>
#include <SceneAPI/SceneData/GraphData/RootBoneData.h>
#include <SceneAPI/SceneCore/Utilities/Reporting.h> #include <SceneAPI/SceneCore/Utilities/Reporting.h>
#include <SceneAPI/SceneData/Rules/CoordinateSystemRule.h> #include <SceneAPI/SceneData/Rules/CoordinateSystemRule.h>
#include <SceneAPIExt/Groups/ActorGroup.h> #include <SceneAPIExt/Groups/ActorGroup.h>
@ -76,6 +80,22 @@ namespace EMotionFX
m_selectedRootBone = selectedRootBone; m_selectedRootBone = selectedRootBone;
} }
void ActorGroup::SetBestMatchingRootBone(const AZ::SceneAPI::Containers::SceneGraph& sceneGraph)
{
auto nameContentView = AZ::SceneAPI::Containers::Views::MakePairView(sceneGraph.GetNameStorage(), sceneGraph.GetContentStorage());
auto graphDownwardsView = AZ::SceneAPI::Containers::Views::MakeSceneGraphDownwardsView<AZ::SceneAPI::Containers::Views::BreadthFirst>(
sceneGraph, sceneGraph.GetRoot(), nameContentView.begin(), true);
for (auto it = graphDownwardsView.begin(); it != graphDownwardsView.end(); ++it)
{
if (it->second && it->second->RTTI_IsTypeOf(AZ::SceneData::GraphData::RootBoneData::TYPEINFO_Uuid()))
{
SetSelectedRootBone(it->first.GetPath());
return;
}
}
}
void ActorGroup::Reflect(AZ::ReflectContext* context) void ActorGroup::Reflect(AZ::ReflectContext* context)
{ {
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context); AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);

@ -45,8 +45,8 @@ namespace EMotionFX
// IActorGroup overrides // IActorGroup overrides
const AZStd::string& GetSelectedRootBone() const override; const AZStd::string& GetSelectedRootBone() const override;
void SetSelectedRootBone(const AZStd::string& selectedRootBone) override; void SetSelectedRootBone(const AZStd::string& selectedRootBone) override;
void SetBestMatchingRootBone(const AZ::SceneAPI::Containers::SceneGraph& sceneGraph) override;
static void Reflect(AZ::ReflectContext* context); static void Reflect(AZ::ReflectContext* context);
static bool IActorGroupVersionConverter(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement); static bool IActorGroupVersionConverter(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement);

@ -10,6 +10,11 @@
#include <AzCore/RTTI/RTTI.h> #include <AzCore/RTTI/RTTI.h>
#include <SceneAPI/SceneCore/DataTypes/Groups/IGroup.h> #include <SceneAPI/SceneCore/DataTypes/Groups/IGroup.h>
namespace AZ::SceneAPI::Containers
{
class SceneGraph;
}
namespace EMotionFX namespace EMotionFX
{ {
namespace Pipeline namespace Pipeline
@ -26,6 +31,7 @@ namespace EMotionFX
virtual const AZStd::string& GetSelectedRootBone() const = 0; virtual const AZStd::string& GetSelectedRootBone() const = 0;
virtual void SetSelectedRootBone(const AZStd::string& selectedRootBone) = 0; virtual void SetSelectedRootBone(const AZStd::string& selectedRootBone) = 0;
virtual void SetBestMatchingRootBone(const AZ::SceneAPI::Containers::SceneGraph& sceneGraph) = 0;
}; };
} }
} }

@ -412,25 +412,24 @@ namespace EMotionFX
void NonUniformMotionData::UpdateDuration() void NonUniformMotionData::UpdateDuration()
{ {
m_duration = 0.0f;
for (const JointData& jointData : m_jointData) for (const JointData& jointData : m_jointData)
{ {
if (!jointData.m_positionTrack.m_times.empty()) if (!jointData.m_positionTrack.m_times.empty())
{ {
m_duration = jointData.m_positionTrack.m_times.back(); m_duration = AZ::GetMax(m_duration, jointData.m_positionTrack.m_times.back());
return;
} }
if (!jointData.m_rotationTrack.m_times.empty()) if (!jointData.m_rotationTrack.m_times.empty())
{ {
m_duration = jointData.m_rotationTrack.m_times.back(); m_duration = AZ::GetMax(m_duration, jointData.m_rotationTrack.m_times.back());
return;
} }
#ifndef EMFX_SCALE_DISABLED #ifndef EMFX_SCALE_DISABLED
if (!jointData.m_scaleTrack.m_times.empty()) if (!jointData.m_scaleTrack.m_times.empty())
{ {
m_duration = jointData.m_scaleTrack.m_times.back(); m_duration = AZ::GetMax(m_duration, jointData.m_scaleTrack.m_times.back());
return;
} }
#endif #endif
} }
@ -439,8 +438,7 @@ namespace EMotionFX
{ {
if (!morphData.m_track.m_times.empty()) if (!morphData.m_track.m_times.empty())
{ {
m_duration = morphData.m_track.m_times.back(); m_duration = AZ::GetMax(m_duration, morphData.m_track.m_times.back());
return;
} }
} }
@ -448,12 +446,9 @@ namespace EMotionFX
{ {
if (!floatData.m_track.m_times.empty()) if (!floatData.m_track.m_times.empty())
{ {
m_duration = floatData.m_track.m_times.back(); m_duration = AZ::GetMax(m_duration, floatData.m_track.m_times.back());
return;
} }
} }
m_duration = 0.0f;
} }
void NonUniformMotionData::AllocateJointPositionSamples(size_t jointDataIndex, size_t numSamples) void NonUniformMotionData::AllocateJointPositionSamples(size_t jointDataIndex, size_t numSamples)

@ -6,7 +6,7 @@
"type": "Code", "type": "Code",
"summary": "The Expression Evaluation Gem provides a method for parsing and executing string expressions in Open 3D Engine.", "summary": "The Expression Evaluation Gem provides a method for parsing and executing string expressions in Open 3D Engine.",
"canonical_tags": ["Gem"], "canonical_tags": ["Gem"],
"user_tags": ["Scripting", "Utiltity"], "user_tags": ["Scripting", "Utility"],
"icon_path": "preview.png", "icon_path": "preview.png",
"requirements": "" "requirements": ""
} }

@ -6,7 +6,7 @@
"type": "Code", "type": "Code",
"summary": "The Game State Samples Gem provides a set of sample game states (built on top of the Game State Gem), including primary user selection, main menu, level loading, level running, and level paused.", "summary": "The Game State Samples Gem provides a set of sample game states (built on top of the Game State Gem), including primary user selection, main menu, level loading, level running, and level paused.",
"canonical_tags": ["Gem"], "canonical_tags": ["Gem"],
"user_tags": ["Gameplay", "Samples", "Assets"], "user_tags": ["Gameplay", "Sample", "Assets"],
"icon_path": "preview.png", "icon_path": "preview.png",
"requirements": "" "requirements": ""
} }

@ -148,7 +148,6 @@ namespace LyShine
{ {
LyShineAllocatorScope::ActivateAllocators(); LyShineAllocatorScope::ActivateAllocators();
LyShineRequestBus::Handler::BusConnect();
UiSystemBus::Handler::BusConnect(); UiSystemBus::Handler::BusConnect();
UiSystemToolsBus::Handler::BusConnect(); UiSystemToolsBus::Handler::BusConnect();
UiFrameworkBus::Handler::BusConnect(); UiFrameworkBus::Handler::BusConnect();
@ -196,7 +195,6 @@ namespace LyShine
UiSystemBus::Handler::BusDisconnect(); UiSystemBus::Handler::BusDisconnect();
UiSystemToolsBus::Handler::BusDisconnect(); UiSystemToolsBus::Handler::BusDisconnect();
UiFrameworkBus::Handler::BusDisconnect(); UiFrameworkBus::Handler::BusDisconnect();
LyShineRequestBus::Handler::BusDisconnect();
CrySystemEventBus::Handler::BusDisconnect(); CrySystemEventBus::Handler::BusDisconnect();
LyShineAllocatorScope::DeactivateAllocators(); LyShineAllocatorScope::DeactivateAllocators();

@ -13,7 +13,6 @@
#include <LmbrCentral/Rendering/MaterialAsset.h> #include <LmbrCentral/Rendering/MaterialAsset.h>
#include <LyShine/LyShineBus.h>
#include <LyShine/Bus/UiSystemBus.h> #include <LyShine/Bus/UiSystemBus.h>
#include <LyShine/Bus/UiCanvasManagerBus.h> #include <LyShine/Bus/UiCanvasManagerBus.h>
#include <LyShine/Bus/Tools/UiSystemToolsBus.h> #include <LyShine/Bus/Tools/UiSystemToolsBus.h>
@ -28,7 +27,6 @@ namespace LyShine
class LyShineSystemComponent class LyShineSystemComponent
: public AZ::Component : public AZ::Component
, protected LyShineRequestBus::Handler
, protected UiSystemBus::Handler , protected UiSystemBus::Handler
, protected UiSystemToolsBus::Handler , protected UiSystemToolsBus::Handler
, protected LyShineAllocatorScope , protected LyShineAllocatorScope

@ -577,7 +577,7 @@ namespace NvCloth
const AZ::Vector3& renderTangent = renderTangents[renderVertexIndex]; const AZ::Vector3& renderTangent = renderTangents[renderVertexIndex];
destTangentsBuffer[index].Set( destTangentsBuffer[index].Set(
renderTangent, renderTangent,
1.0f); -1.0f); // Shader function ConstructTBN inverts w to change bitangent sign, but the bitangents passed are already corrected, so passing -1.0 to counteract.
} }
if (destBitangentsBuffer) if (destBitangentsBuffer)

@ -11,7 +11,7 @@ namespace NvCloth
{ {
namespace namespace
{ {
const float Tolerance = 0.0001f; const float Tolerance = 1e-7f;
} }
bool TangentSpaceHelper::CalculateNormals( bool TangentSpaceHelper::CalculateNormals(
@ -33,7 +33,8 @@ namespace NvCloth
const size_t vertexCount = vertices.size(); const size_t vertexCount = vertices.size();
// Reset results // Reset results
outNormals.resize(vertexCount, AZ::Vector3::CreateZero()); outNormals.resize(vertexCount);
AZStd::fill(outNormals.begin(), outNormals.end(), AZ::Vector3::CreateZero());
// calculate the normals per triangle // calculate the normals per triangle
for (size_t i = 0; i < triangleCount; ++i) for (size_t i = 0; i < triangleCount; ++i)
@ -114,8 +115,10 @@ namespace NvCloth
const size_t vertexCount = vertices.size(); const size_t vertexCount = vertices.size();
// Reset results // Reset results
outTangents.resize(vertexCount, AZ::Vector3::CreateZero()); outTangents.resize(vertexCount);
outBitangents.resize(vertexCount, AZ::Vector3::CreateZero()); outBitangents.resize(vertexCount);
AZStd::fill(outTangents.begin(), outTangents.end(), AZ::Vector3::CreateZero());
AZStd::fill(outBitangents.begin(), outBitangents.end(), AZ::Vector3::CreateZero());
// calculate the base vectors per triangle // calculate the base vectors per triangle
for (size_t i = 0; i < triangleCount; ++i) for (size_t i = 0; i < triangleCount; ++i)
@ -192,9 +195,12 @@ namespace NvCloth
const size_t vertexCount = vertices.size(); const size_t vertexCount = vertices.size();
// Reset results // Reset results
outTangents.resize(vertexCount, AZ::Vector3::CreateZero()); outTangents.resize(vertexCount);
outBitangents.resize(vertexCount, AZ::Vector3::CreateZero()); outBitangents.resize(vertexCount);
outNormals.resize(vertexCount, AZ::Vector3::CreateZero()); outNormals.resize(vertexCount);
AZStd::fill(outTangents.begin(), outTangents.end(), AZ::Vector3::CreateZero());
AZStd::fill(outBitangents.begin(), outBitangents.end(), AZ::Vector3::CreateZero());
AZStd::fill(outNormals.begin(), outNormals.end(), AZ::Vector3::CreateZero());
// calculate the base vectors per triangle // calculate the base vectors per triangle
for (size_t i = 0; i < triangleCount; ++i) for (size_t i = 0; i < triangleCount; ++i)

@ -124,6 +124,9 @@ namespace UnitTest
const AZStd::vector<AZ::Vector4>& motionConstraints = clothConstraints->GetMotionConstraints(); const AZStd::vector<AZ::Vector4>& motionConstraints = clothConstraints->GetMotionConstraints();
EXPECT_TRUE(motionConstraints.size() == SimulationParticles.size()); EXPECT_TRUE(motionConstraints.size() == SimulationParticles.size());
EXPECT_THAT(motionConstraints[0].GetAsVector3(), IsCloseTolerance(SimulationParticles[0].GetAsVector3(), Tolerance));
EXPECT_THAT(motionConstraints[1].GetAsVector3(), IsCloseTolerance(SimulationParticles[1].GetAsVector3(), Tolerance));
EXPECT_THAT(motionConstraints[2].GetAsVector3(), IsCloseTolerance(SimulationParticles[2].GetAsVector3(), Tolerance));
EXPECT_NEAR(motionConstraints[0].GetW(), 6.0f, Tolerance); EXPECT_NEAR(motionConstraints[0].GetW(), 6.0f, Tolerance);
EXPECT_NEAR(motionConstraints[1].GetW(), 0.0f, Tolerance); EXPECT_NEAR(motionConstraints[1].GetW(), 0.0f, Tolerance);
EXPECT_NEAR(motionConstraints[2].GetW(), 0.0f, Tolerance); EXPECT_NEAR(motionConstraints[2].GetW(), 0.0f, Tolerance);
@ -277,6 +280,9 @@ namespace UnitTest
const AZStd::vector<AZ::Vector4>& separationConstraints = clothConstraints->GetSeparationConstraints(); const AZStd::vector<AZ::Vector4>& separationConstraints = clothConstraints->GetSeparationConstraints();
EXPECT_TRUE(motionConstraints.size() == newParticles.size()); EXPECT_TRUE(motionConstraints.size() == newParticles.size());
EXPECT_THAT(motionConstraints[0].GetAsVector3(), IsCloseTolerance(newParticles[0].GetAsVector3(), Tolerance));
EXPECT_THAT(motionConstraints[1].GetAsVector3(), IsCloseTolerance(newParticles[1].GetAsVector3(), Tolerance));
EXPECT_THAT(motionConstraints[2].GetAsVector3(), IsCloseTolerance(newParticles[2].GetAsVector3(), Tolerance));
EXPECT_NEAR(motionConstraints[0].GetW(), 3.0f, Tolerance); EXPECT_NEAR(motionConstraints[0].GetW(), 3.0f, Tolerance);
EXPECT_NEAR(motionConstraints[1].GetW(), 1.5f, Tolerance); EXPECT_NEAR(motionConstraints[1].GetW(), 1.5f, Tolerance);
EXPECT_NEAR(motionConstraints[2].GetW(), 0.0f, Tolerance); EXPECT_NEAR(motionConstraints[2].GetW(), 0.0f, Tolerance);
@ -285,8 +291,8 @@ namespace UnitTest
EXPECT_NEAR(separationConstraints[0].GetW(), 3.0f, Tolerance); EXPECT_NEAR(separationConstraints[0].GetW(), 3.0f, Tolerance);
EXPECT_NEAR(separationConstraints[1].GetW(), 1.5f, Tolerance); EXPECT_NEAR(separationConstraints[1].GetW(), 1.5f, Tolerance);
EXPECT_NEAR(separationConstraints[2].GetW(), 0.3f, Tolerance); EXPECT_NEAR(separationConstraints[2].GetW(), 0.3f, Tolerance);
EXPECT_THAT(separationConstraints[0].GetAsVector3(), IsCloseTolerance(AZ::Vector3(-3.03902f, 2.80752f, 3.80752f), Tolerance)); EXPECT_THAT(separationConstraints[0].GetAsVector3(), IsCloseTolerance(AZ::Vector3(0.0f, 3.53553f, 4.53553f), Tolerance));
EXPECT_THAT(separationConstraints[1].GetAsVector3(), IsCloseTolerance(AZ::Vector3(-1.41659f, 0.651243f, -0.348757f), Tolerance)); EXPECT_THAT(separationConstraints[1].GetAsVector3(), IsCloseTolerance(AZ::Vector3(0.0f, 2.06066f, 1.06066f), Tolerance));
EXPECT_THAT(separationConstraints[2].GetAsVector3(), IsCloseTolerance(AZ::Vector3(6.15313f, -0.876132f, 0.123868f), Tolerance)); EXPECT_THAT(separationConstraints[2].GetAsVector3(), IsCloseTolerance(AZ::Vector3(1.0f, -3.74767f, -2.74767f), Tolerance));
} }
} // namespace UnitTest } // namespace UnitTest

@ -6,7 +6,7 @@
"type": "Tool", "type": "Tool",
"summary": "The Script Canvas Gem provides Open 3D Engine's visual scripting environment, Script Canvas.", "summary": "The Script Canvas Gem provides Open 3D Engine's visual scripting environment, Script Canvas.",
"canonical_tags": ["Gem"], "canonical_tags": ["Gem"],
"user_tags": ["Scripting", "Tools", "Utiltiy"], "user_tags": ["Scripting", "Tools", "Utility"],
"icon_path": "preview.png", "icon_path": "preview.png",
"requirements": "" "requirements": ""
} }

@ -6,7 +6,7 @@
"type": "Code", "type": "Code",
"summary": "The Surface Data Gem provides functionality to emit signals or tags from surfaces such as meshes and terrain.", "summary": "The Surface Data Gem provides functionality to emit signals or tags from surfaces such as meshes and terrain.",
"canonical_tags": ["Gem"], "canonical_tags": ["Gem"],
"user_tags": ["Environment", "Utiltiy", "Design"], "user_tags": ["Environment", "Utility", "Design"],
"icon_path": "preview.png", "icon_path": "preview.png",
"requirements": "" "requirements": ""
} }

@ -529,6 +529,12 @@ function(ly_force_download_package package_name)
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf ${temp_download_target} execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf ${temp_download_target}
WORKING_DIRECTORY ${final_folder} COMMAND_ECHO STDOUT OUTPUT_VARIABLE unpack_result) WORKING_DIRECTORY ${final_folder} COMMAND_ECHO STDOUT OUTPUT_VARIABLE unpack_result)
# For the runtime dependencies cases, we need the timestamps of the files coming from 3rdParty to be newer than the ones
# from the output so the new versions get copied over. The untar from the previous step preserves timestamps so they
# can produce binaries with older timestamps to the ones that are in the build output.
file(GLOB_RECURSE package_files LIST_DIRECTORIES false ${final_folder}/*)
file(TOUCH_NOCREATE ${package_files})
if (NOT ${unpack_result} EQUAL 0) if (NOT ${unpack_result} EQUAL 0)
message(SEND_ERROR "ly_package: required package {package_name} could not be unpacked. Compile may fail! Enable LY_PACKAGE_DEBUG to debug.") message(SEND_ERROR "ly_package: required package {package_name} could not be unpacked. Compile may fail! Enable LY_PACKAGE_DEBUG to debug.")
return() return()

@ -124,3 +124,42 @@ function(ly_file_read path content)
set(${content} ${file_content} PARENT_SCOPE) set(${content} ${file_content} PARENT_SCOPE)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${path}) set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${path})
endfunction() endfunction()
#! ly_get_last_path_segment_concat_sha256 : Concatenates the last path segment of the absolute path
# with the first 8 characters of the absolute path SHA256 hash to make a unique relative path segment
function(ly_get_last_path_segment_concat_sha256 absolute_path output_path)
string(SHA256 target_source_hash ${absolute_path})
string(SUBSTRING ${target_source_hash} 0 8 target_source_hash)
cmake_path(GET absolute_path FILENAME last_path_segment)
cmake_path(SET last_path_segment_sha256_path "${last_path_segment}-${target_source_hash}")
set(${output_path} ${last_path_segment_sha256_path} PARENT_SCOPE)
endfunction()
#! ly_get_engine_relative_source_dir: Attempts to form a path relative to the BASE_DIRECTORY.
# If that fails the last path segment of the absolute_target_source_dir concatenated with a SHA256 hash to form a target directory
# \arg:BASE_DIRECTORY - Directory to base relative path against. Defaults to LY_ROOT_FOLDER
function(ly_get_engine_relative_source_dir absolute_target_source_dir output_source_dir)
set(options)
set(oneValueArgs BASE_DIRECTORY)
set(multiValueArgs)
cmake_parse_arguments(ly_get_engine_relative_source_dir "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(NOT ly_get_engine_relative_source_dir_BASE_DIRECTORY)
set(ly_get_engine_relative_source_dir_BASE_DIRECTORY ${LY_ROOT_FOLDER})
endif()
# Get a relative target source directory to the LY root folder if possible
# Otherwise use the final component name
cmake_path(IS_PREFIX LY_ROOT_FOLDER ${absolute_target_source_dir} is_target_source_dir_subdirectory_of_engine)
if(is_target_source_dir_subdirectory_of_engine)
cmake_path(RELATIVE_PATH absolute_target_source_dir BASE_DIRECTORY ${LY_ROOT_FOLDER} OUTPUT_VARIABLE relative_target_source_dir)
else()
ly_get_last_path_segment_concat_sha256(${absolute_target_source_dir} target_source_dir_last_path_segment)
unset(relative_target_source_dir)
cmake_path(APPEND relative_target_source_dir "External" ${target_source_dir_last_path_segment})
endif()
set(${output_source_dir} ${relative_target_source_dir} PARENT_SCOPE)
endfunction()

@ -90,6 +90,13 @@ function(ly_create_alias)
# Replace the CMake list separator with a space to replicate the space separated TARGETS arguments # Replace the CMake list separator with a space to replicate the space separated TARGETS arguments
string(REPLACE ";" " " create_alias_args "${ly_create_alias_NAME},${ly_create_alias_NAMESPACE},${ly_create_alias_TARGETS}") string(REPLACE ";" " " create_alias_args "${ly_create_alias_NAME},${ly_create_alias_NAMESPACE},${ly_create_alias_TARGETS}")
set_property(DIRECTORY APPEND PROPERTY LY_CREATE_ALIAS_ARGUMENTS "${create_alias_args}") set_property(DIRECTORY APPEND PROPERTY LY_CREATE_ALIAS_ARGUMENTS "${create_alias_args}")
# Store the directory path in the GLOBAL property so that it can be accessed
# in the layout install logic. Skip if the directory has already been added
get_property(ly_all_target_directories GLOBAL PROPERTY LY_ALL_TARGET_DIRECTORIES)
if(NOT CMAKE_CURRENT_SOURCE_DIR IN_LIST ly_all_target_directories)
set_property(GLOBAL APPEND PROPERTY LY_ALL_TARGET_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR})
endif()
endfunction() endfunction()
# ly_enable_gems # ly_enable_gems

@ -391,10 +391,10 @@ function(ly_delayed_target_link_libraries)
endif() endif()
if(item_type STREQUAL MODULE_LIBRARY) if(item_type STREQUAL MODULE_LIBRARY)
target_include_directories(${target} ${visibility} $<TARGET_PROPERTY:${item},INTERFACE_INCLUDE_DIRECTORIES>) target_include_directories(${target} ${visibility} $<GENEX_EVAL:$<TARGET_PROPERTY:${item},INTERFACE_INCLUDE_DIRECTORIES>>)
target_link_libraries(${target} ${visibility} $<TARGET_PROPERTY:${item},INTERFACE_LINK_LIBRARIES>) target_link_libraries(${target} ${visibility} $<GENEX_EVAL:$<TARGET_PROPERTY:${item},INTERFACE_LINK_LIBRARIES>>)
target_compile_definitions(${target} ${visibility} $<TARGET_PROPERTY:${item},INTERFACE_COMPILE_DEFINITIONS>) target_compile_definitions(${target} ${visibility} $<GENEX_EVAL:$<TARGET_PROPERTY:${item},INTERFACE_COMPILE_DEFINITIONS>>)
target_compile_options(${target} ${visibility} $<TARGET_PROPERTY:${item},INTERFACE_COMPILE_OPTIONS>) target_compile_options(${target} ${visibility} $<GENEX_EVAL:$<TARGET_PROPERTY:${item},INTERFACE_COMPILE_OPTIONS>>)
else() else()
ly_parse_third_party_dependencies(${item}) ly_parse_third_party_dependencies(${item})
target_link_libraries(${target} ${visibility} ${item}) target_link_libraries(${target} ${visibility} ${item})
@ -659,7 +659,12 @@ function(ly_get_vs_folder_directory absolute_target_source_dir output_source_dir
if(is_target_prefix_of_engine_root) if(is_target_prefix_of_engine_root)
cmake_path(RELATIVE_PATH absolute_target_source_dir BASE_DIRECTORY ${LY_ROOT_FOLDER} OUTPUT_VARIABLE relative_target_source_dir) cmake_path(RELATIVE_PATH absolute_target_source_dir BASE_DIRECTORY ${LY_ROOT_FOLDER} OUTPUT_VARIABLE relative_target_source_dir)
else() else()
cmake_path(GET absolute_target_source_dir RELATIVE_PART relative_target_source_dir) cmake_path(IS_PREFIX CMAKE_SOURCE_DIR ${absolute_target_source_dir} is_target_prefix_of_source_dir)
if(is_target_prefix_of_source_dir)
cmake_path(RELATIVE_PATH absolute_target_source_dir BASE_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE relative_target_source_dir)
else()
cmake_path(GET absolute_target_source_dir RELATIVE_PART relative_target_source_dir)
endif()
endif() endif()
set(${output_source_dir} ${relative_target_source_dir} PARENT_SCOPE) set(${output_source_dir} ${relative_target_source_dir} PARENT_SCOPE)

@ -125,37 +125,32 @@ install(FILES ${_cmake_package_dest}
DESTINATION ./Tools/Redistributables/CMake DESTINATION ./Tools/Redistributables/CMake
) )
# temporary workaround for acquiring the 3rd party SPDX license manifest, the desired location is from # the version string and git tags are intended to be synchronized so it should be safe to use that instead
# another git repository that's private. once it's public, only how the URL is formed should change # of directly calling into git which could get messy in certain scenarios
set(LY_INSTALLER_3RD_PARTY_LICENSE_URL "" CACHE STRING "URL to the 3rd party SPDX license manifest file for inclusion in packaging.") if(${CPACK_PACKAGE_VERSION} VERSION_GREATER "0.0.0.0")
set(_3rd_party_license_filename NOTICES.txt)
if(${LY_VERSION_STRING} VERSION_GREATER "0.0.0.0" AND NOT LY_INSTALLER_3RD_PARTY_LICENSE_URL)
message(FATAL_ERROR "Missing required URL for the 3rd party SPDX license manifest file. " set(_3rd_party_license_url "https://raw.githubusercontent.com/o3de/3p-package-source/${CPACK_PACKAGE_VERSION}/${_3rd_party_license_filename}")
"Please specifiy where to acquire the file via LY_INSTALLER_3RD_PARTY_LICENSE_URL") set(_3rd_party_license_dest ${CPACK_BINARY_DIR}/${_3rd_party_license_filename})
endif()
# use the plain file downloader as we don't have the file hash available and using a dummy will
string(REPLACE "/" ";" _url_components ${LY_INSTALLER_3RD_PARTY_LICENSE_URL}) # delete the file once it fails hash verification
list(POP_BACK _url_components _3rd_party_license_filename) file(DOWNLOAD
${_3rd_party_license_url}
set(_3rd_party_license_dest ${CPACK_BINARY_DIR}/${_3rd_party_license_filename}) ${_3rd_party_license_dest}
STATUS _status
# use the plain file downloader as we don't have the file hash available and using a dummy will TLS_VERIFY ON
# delete the file once it fails hash verification
file(DOWNLOAD
${LY_INSTALLER_3RD_PARTY_LICENSE_URL}
${_3rd_party_license_dest}
STATUS _status
TLS_VERIFY ON
)
list(POP_FRONT _status _status_code)
if (${_status_code} EQUAL 0 AND EXISTS ${_3rd_party_license_dest})
install(FILES ${_3rd_party_license_dest}
DESTINATION .
) )
else() list(POP_FRONT _status _status_code)
file(REMOVE ${_3rd_party_license_dest})
message(FATAL_ERROR "Failed to acquire the 3rd Party license manifest file. Error: ${_status}") if (${_status_code} EQUAL 0 AND EXISTS ${_3rd_party_license_dest})
install(FILES ${_3rd_party_license_dest}
DESTINATION .
)
else()
file(REMOVE ${_3rd_party_license_dest})
message(FATAL_ERROR "Failed to acquire the 3rd Party license manifest file at ${_3rd_party_license_url}. Error: ${_status}")
endif()
endif() endif()
# checks for and removes trailing slash # checks for and removes trailing slash

@ -9,6 +9,7 @@ SPDX-License-Identifier: Apache-2.0 OR MIT
<PropertyGroup> <PropertyGroup>
<UseMultiToolTask>true</UseMultiToolTask> <UseMultiToolTask>true</UseMultiToolTask>
<EnforceProcessCountAcrossBuilds>true</EnforceProcessCountAcrossBuilds> <EnforceProcessCountAcrossBuilds>true</EnforceProcessCountAcrossBuilds>
@VCPKG_CONFIGURATION_MAPPING@
</PropertyGroup> </PropertyGroup>
<ItemDefinitionGroup> <ItemDefinitionGroup>
<ClCompile> <ClCompile>

@ -5,8 +5,19 @@
# #
# #
include(cmake/FileUtil.cmake)
set(CMAKE_INSTALL_MESSAGE NEVER) # Simplify messages to reduce output noise set(CMAKE_INSTALL_MESSAGE NEVER) # Simplify messages to reduce output noise
define_property(TARGET PROPERTY LY_INSTALL_GENERATE_RUN_TARGET
BRIEF_DOCS "Defines if a \"RUN\" targets should be created when installing this target Gem"
FULL_DOCS [[
Property which is set on targets that should generate a "RUN"
target when installed. This \"RUN\" target helps to run the
binary from the installed location directly from the IDE.
]]
)
ly_set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME Core) ly_set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME Core)
cmake_path(RELATIVE_PATH CMAKE_RUNTIME_OUTPUT_DIRECTORY BASE_DIRECTORY ${CMAKE_BINARY_DIR} OUTPUT_VARIABLE runtime_output_directory) cmake_path(RELATIVE_PATH CMAKE_RUNTIME_OUTPUT_DIRECTORY BASE_DIRECTORY ${CMAKE_BINARY_DIR} OUTPUT_VARIABLE runtime_output_directory)
@ -18,26 +29,6 @@ cmake_path(RELATIVE_PATH CMAKE_LIBRARY_OUTPUT_DIRECTORY BASE_DIRECTORY ${CMAKE_B
set(install_output_folder "\${CMAKE_INSTALL_PREFIX}/${runtime_output_directory}/${PAL_PLATFORM_NAME}/$<CONFIG>") set(install_output_folder "\${CMAKE_INSTALL_PREFIX}/${runtime_output_directory}/${PAL_PLATFORM_NAME}/$<CONFIG>")
function(ly_get_engine_relative_source_dir absolute_target_source_dir output_source_dir)
# Get a relative target source directory to the LY root folder if possible
# Otherwise use the final component name
cmake_path(IS_PREFIX LY_ROOT_FOLDER ${absolute_target_source_dir} is_target_prefix_of_engine_root)
if(is_target_prefix_of_engine_root)
cmake_path(RELATIVE_PATH absolute_target_source_dir BASE_DIRECTORY ${LY_ROOT_FOLDER} OUTPUT_VARIABLE relative_target_source_dir)
else()
# In this case the target source directory is outside of the engine root of the target source directory and concatenate the first
# is used first 8 characters of the absolute path SHA256 hash to make a unique relative directory
# that can be used to install the generated CMakeLists.txt
# of a SHA256 hash
string(SHA256 target_source_hash ${absolute_target_source_dir})
string(SUBSTRING ${target_source_hash} 0 8 target_source_hash)
cmake_path(GET absolute_target_source_dir FILENAME target_source_dirname)
cmake_path(SET relative_target_source_dir "${target_source_dirname}-${target_source_hash}")
endif()
set(${output_source_dir} ${relative_target_source_dir} PARENT_SCOPE)
endfunction()
#! ly_setup_target: Setup the data needed to re-create the cmake target commands for a single target #! ly_setup_target: Setup the data needed to re-create the cmake target commands for a single target
function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_target_source_dir) function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_target_source_dir)
# De-alias target name # De-alias target name
@ -82,6 +73,7 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar
PATTERN *.h PATTERN *.h
PATTERN *.hpp PATTERN *.hpp
PATTERN *.inl PATTERN *.inl
PATTERN *.hxx
) )
endif() endif()
endforeach() endforeach()
@ -122,15 +114,19 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar
set(NAMESPACE_PLACEHOLDER "") set(NAMESPACE_PLACEHOLDER "")
set(NAME_PLACEHOLDER ${TARGET_NAME}) set(NAME_PLACEHOLDER ${TARGET_NAME})
endif() endif()
get_target_property(should_create_helper ${TARGET_NAME} LY_INSTALL_GENERATE_RUN_TARGET)
if(should_create_helper)
set(NAME_PLACEHOLDER ${NAME_PLACEHOLDER}.Imported)
endif()
set(TARGET_TYPE_PLACEHOLDER "") set(TARGET_TYPE_PLACEHOLDER "")
get_target_property(target_type ${NAME_PLACEHOLDER} TYPE) get_target_property(target_type ${TARGET_NAME} TYPE)
# Remove the _LIBRARY since we dont need to pass that to ly_add_targets # Remove the _LIBRARY since we dont need to pass that to ly_add_targets
string(REPLACE "_LIBRARY" "" TARGET_TYPE_PLACEHOLDER ${target_type}) string(REPLACE "_LIBRARY" "" TARGET_TYPE_PLACEHOLDER ${target_type})
# For HEADER_ONLY libs we end up generating "INTERFACE" libraries, need to specify HEADERONLY instead # For HEADER_ONLY libs we end up generating "INTERFACE" libraries, need to specify HEADERONLY instead
string(REPLACE "INTERFACE" "HEADERONLY" TARGET_TYPE_PLACEHOLDER ${TARGET_TYPE_PLACEHOLDER}) string(REPLACE "INTERFACE" "HEADERONLY" TARGET_TYPE_PLACEHOLDER ${TARGET_TYPE_PLACEHOLDER})
if(TARGET_TYPE_PLACEHOLDER STREQUAL "MODULE") if(TARGET_TYPE_PLACEHOLDER STREQUAL "MODULE")
get_target_property(gem_module ${NAME_PLACEHOLDER} GEM_MODULE) get_target_property(gem_module ${TARGET_NAME} GEM_MODULE)
if(gem_module) if(gem_module)
set(TARGET_TYPE_PLACEHOLDER "GEM_MODULE") set(TARGET_TYPE_PLACEHOLDER "GEM_MODULE")
endif() endif()
@ -163,7 +159,6 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar
unset(RUNTIME_DEPENDENCIES_PLACEHOLDER) unset(RUNTIME_DEPENDENCIES_PLACEHOLDER)
endif() endif()
get_target_property(inteface_build_dependencies_props ${TARGET_NAME} INTERFACE_LINK_LIBRARIES) get_target_property(inteface_build_dependencies_props ${TARGET_NAME} INTERFACE_LINK_LIBRARIES)
unset(INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER) unset(INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER)
if(inteface_build_dependencies_props) if(inteface_build_dependencies_props)
@ -187,6 +182,23 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar
list(REMOVE_DUPLICATES INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER) list(REMOVE_DUPLICATES INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER)
string(REPLACE ";" "\n" INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER "${INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER}") string(REPLACE ";" "\n" INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER "${INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER}")
# If the target is an executable/application, add a custom target so we can debug the target in project-centric workflow
if(should_create_helper)
string(REPLACE ".Imported" "" RUN_TARGET_NAME ${NAME_PLACEHOLDER})
set(target_types_with_debugging_helper EXECUTABLE APPLICATION)
if(NOT target_type IN_LIST target_types_with_debugging_helper)
message(FATAL_ERROR "Cannot generate a RUN target for ${TARGET_NAME}, type is ${target_type}")
endif()
set(TARGET_RUN_HELPER
"add_custom_target(${RUN_TARGET_NAME})
set_target_properties(${RUN_TARGET_NAME} PROPERTIES
FOLDER \"CMakePredefinedTargets/SDK\"
VS_DEBUGGER_COMMAND \$<GENEX_EVAL:\$<TARGET_PROPERTY:${NAME_PLACEHOLDER},IMPORTED_LOCATION>>
VS_DEBUGGER_COMMAND_ARGUMENTS \"--project-path=\${LY_DEFAULT_PROJECT_PATH}\"
)"
)
endif()
# Config file # Config file
set(target_file_contents "# Generated by O3DE install\n\n") set(target_file_contents "# Generated by O3DE install\n\n")
if(NOT target_type STREQUAL INTERFACE_LIBRARY) if(NOT target_type STREQUAL INTERFACE_LIBRARY)
@ -199,13 +211,13 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar
set(target_location "\${LY_ROOT_FOLDER}/${library_output_directory}/${PAL_PLATFORM_NAME}/$<CONFIG>/${target_library_output_subdirectory}/$<TARGET_FILE_NAME:${TARGET_NAME}>") set(target_location "\${LY_ROOT_FOLDER}/${library_output_directory}/${PAL_PLATFORM_NAME}/$<CONFIG>/${target_library_output_subdirectory}/$<TARGET_FILE_NAME:${TARGET_NAME}>")
elseif(target_type STREQUAL SHARED_LIBRARY) elseif(target_type STREQUAL SHARED_LIBRARY)
string(APPEND target_file_contents string(APPEND target_file_contents
"set_property(TARGET ${TARGET_NAME} "set_property(TARGET ${NAME_PLACEHOLDER}
APPEND_STRING PROPERTY IMPORTED_IMPLIB APPEND_STRING PROPERTY IMPORTED_IMPLIB
$<$<CONFIG:$<CONFIG>$<ANGLE-R>:\"\${LY_ROOT_FOLDER}/${archive_output_directory}/${PAL_PLATFORM_NAME}/$<CONFIG>/$<TARGET_LINKER_FILE_NAME:${TARGET_NAME}>\"$<ANGLE-R> $<$<CONFIG:$<CONFIG>$<ANGLE-R>:\"\${LY_ROOT_FOLDER}/${archive_output_directory}/${PAL_PLATFORM_NAME}/$<CONFIG>/$<TARGET_LINKER_FILE_NAME:${TARGET_NAME}>\"$<ANGLE-R>
) )
") ")
string(APPEND target_file_contents string(APPEND target_file_contents
"set_property(TARGET ${TARGET_NAME} "set_property(TARGET ${NAME_PLACEHOLDER}
PROPERTY IMPORTED_IMPLIB_$<UPPER_CASE:$<CONFIG>> PROPERTY IMPORTED_IMPLIB_$<UPPER_CASE:$<CONFIG>>
\"\${LY_ROOT_FOLDER}/${archive_output_directory}/${PAL_PLATFORM_NAME}/$<CONFIG>/$<TARGET_LINKER_FILE_NAME:${TARGET_NAME}>\" \"\${LY_ROOT_FOLDER}/${archive_output_directory}/${PAL_PLATFORM_NAME}/$<CONFIG>/$<TARGET_LINKER_FILE_NAME:${TARGET_NAME}>\"
) )
@ -217,11 +229,11 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar
if(target_location) if(target_location)
string(APPEND target_file_contents string(APPEND target_file_contents
"set_property(TARGET ${TARGET_NAME} "set_property(TARGET ${NAME_PLACEHOLDER}
APPEND_STRING PROPERTY IMPORTED_LOCATION APPEND_STRING PROPERTY IMPORTED_LOCATION
$<$<CONFIG:$<CONFIG>$<ANGLE-R>:${target_location}$<ANGLE-R> $<$<CONFIG:$<CONFIG>$<ANGLE-R>:${target_location}$<ANGLE-R>
) )
set_property(TARGET ${TARGET_NAME} set_property(TARGET ${NAME_PLACEHOLDER}
PROPERTY IMPORTED_LOCATION_$<UPPER_CASE:$<CONFIG>> PROPERTY IMPORTED_LOCATION_$<UPPER_CASE:$<CONFIG>>
${target_location} ${target_location}
) )
@ -254,7 +266,6 @@ endfunction()
#! ly_setup_subdirectory: setup all targets in the subdirectory #! ly_setup_subdirectory: setup all targets in the subdirectory
function(ly_setup_subdirectory absolute_target_source_dir) function(ly_setup_subdirectory absolute_target_source_dir)
# Get the target source directory relative to the LY roo folder
ly_get_engine_relative_source_dir(${absolute_target_source_dir} relative_target_source_dir) ly_get_engine_relative_source_dir(${absolute_target_source_dir} relative_target_source_dir)
# The builtin BUILDSYSTEM_TARGETS property isn't being used here as that returns the de-alised # The builtin BUILDSYSTEM_TARGETS property isn't being used here as that returns the de-alised
@ -540,56 +551,74 @@ function(ly_setup_others)
) )
# Exclude transient artifacts that shouldn't be copied to the install layout # Exclude transient artifacts that shouldn't be copied to the install layout
list(FILTER external_subdir_files EXCLUDE REGEX "/([Bb]uild|[Cc]ache|[Uu]ser)$") list(FILTER external_subdir_files EXCLUDE REGEX "/([Bb]uild|[Cc]ache|[Uu]ser)$")
list(APPEND filtered_asset_paths ${external_subdir_files}) # Storing a "mapping" of gem candidate directories, to external_subdirectory files using
# a DIRECTORY property for the "value" and the GLOBAL property for the "key"
set_property(DIRECTORY ${gem_candidate_dir} APPEND PROPERTY directory_filtered_asset_paths "${external_subdir_files}")
set_property(GLOBAL APPEND PROPERTY global_gem_candidate_dirs_prop ${gem_candidate_dir})
endforeach() endforeach()
# At this point the filtered_assets_paths contains the list of all directories and files # Iterate over each gem candidate directories and read populate a directory property
# that are non-excluded candidates that can be scanned for target directories and files # containing the files to copy over
# to copy over to the install layout get_property(gem_candidate_dirs GLOBAL PROPERTY global_gem_candidate_dirs_prop)
foreach(filtered_asset_path IN LISTS filtered_asset_paths) foreach(gem_candidate_dir IN LISTS gem_candidate_dirs)
if(IS_DIRECTORY ${filtered_asset_path}) get_property(filtered_asset_paths DIRECTORY ${gem_candidate_dir} PROPERTY directory_filtered_asset_paths)
file(GLOB_RECURSE ly_get_last_path_segment_concat_sha256(${gem_candidate_dir} last_gem_root_path_segment)
recurse_assets_paths # Check if the gem is a subdirectory of the engine
LIST_DIRECTORIES TRUE cmake_path(IS_PREFIX LY_ROOT_FOLDER ${gem_candidate_dir} is_gem_subdirectory_of_engine)
"${filtered_asset_path}/*"
) # At this point the filtered_assets_paths contains the list of all directories and files
set(gem_file_paths ${recurse_assets_paths}) # that are non-excluded candidates that can be scanned for target directories and files
# Make sure to prepend the current path iteration to the gem_dirs_path to filter # to copy over to the install layout
set(gem_dir_paths ${filtered_asset_path} ${recurse_assets_paths}) foreach(filtered_asset_path IN LISTS filtered_asset_paths)
if(IS_DIRECTORY ${filtered_asset_path})
# Gather directories to copy over file(GLOB_RECURSE
# Currently only the Assets, Registry and Config directories are copied over recurse_assets_paths
list(FILTER gem_dir_paths INCLUDE REGEX "/(Assets|Registry|Config)$") LIST_DIRECTORIES TRUE
list(APPEND gems_assets_dir_path ${gem_dir_paths}) "${filtered_asset_path}/*"
else() )
set(gem_file_paths ${filtered_asset_path}) set(gem_file_paths ${recurse_assets_paths})
endif() # Make sure to prepend the current path iteration to the gem_dirs_path to filter
set(gem_dir_paths ${filtered_asset_path} ${recurse_assets_paths})
# Gather directories to copy over
# Currently only the Assets, Registry and Config directories are copied over
list(FILTER gem_dir_paths INCLUDE REGEX "/(Assets|Registry|Config|Editor/Scripts)$")
set_property(DIRECTORY ${gem_candidate_dir} APPEND PROPERTY gems_assets_paths ${gem_dir_paths})
else()
set(gem_file_paths ${filtered_asset_path})
endif()
# Gather files to copy over # Gather files to copy over
# Currently only the gem.json file is copied over # Currently only the gem.json file is copied over
list(FILTER gem_file_paths INCLUDE REGEX "/(gem.json)$") list(FILTER gem_file_paths INCLUDE REGEX "/(gem.json|preview.png)$")
list(APPEND gems_assets_file_path "${gem_file_paths}") set_property(DIRECTORY ${gem_candidate_dir} APPEND PROPERTY gems_assets_paths "${gem_file_paths}")
endforeach() endforeach()
# gem directories to install # gem directories and files to install
foreach(gem_absolute_dir_path ${gems_assets_dir_path}) get_property(gems_assets_paths DIRECTORY ${gem_candidate_dir} PROPERTY gems_assets_paths)
cmake_path(RELATIVE_PATH gem_absolute_dir_path BASE_DIRECTORY ${LY_ROOT_FOLDER} OUTPUT_VARIABLE gem_relative_dir_path) foreach(gem_absolute_path IN LISTS gems_assets_paths)
if (EXISTS ${gem_absolute_dir_path}) if(is_gem_subdirectory_of_engine)
# The trailing slash is IMPORTANT here as that is needed to prevent cmake_path(RELATIVE_PATH gem_absolute_path BASE_DIRECTORY ${LY_ROOT_FOLDER} OUTPUT_VARIABLE gem_install_dest_dir)
# the "Assets" folder from being copied underneath the <gem-root>/Assets folder else()
install(DIRECTORY "${gem_absolute_dir_path}/" # The gem resides outside of the LY_ROOT_FOLDER, so the destination is made relative to the
DESTINATION ${gem_relative_dir_path} # gem candidate directory and placed under the "External" directory"
) # directory
endif() cmake_path(RELATIVE_PATH gem_absolute_path BASE_DIRECTORY ${gem_candidate_dir} OUTPUT_VARIABLE gem_relative_path)
endforeach() unset(gem_install_dest_dir)
cmake_path(APPEND gem_install_dest_dir "External" ${last_gem_root_path_segment} ${gem_relative_path})
endif()
cmake_path(GET gem_install_dest_dir PARENT_PATH gem_install_dest_dir)
if (NOT gem_install_dest_dir)
cmake_path(SET gem_install_dest_dir .)
endif()
if(IS_DIRECTORY ${gem_absolute_path})
install(DIRECTORY "${gem_absolute_path}" DESTINATION ${gem_install_dest_dir})
elseif (EXISTS ${gem_absolute_path})
install(FILES ${gem_absolute_path} DESTINATION ${gem_install_dest_dir})
endif()
endforeach()
# gem files to install
foreach(gem_absolute_file_path ${gems_assets_file_path})
cmake_path(RELATIVE_PATH gem_absolute_file_path BASE_DIRECTORY ${LY_ROOT_FOLDER} OUTPUT_VARIABLE gem_relative_file_path)
cmake_path(GET gem_relative_file_path PARENT_PATH gem_relative_parent_dir)
install(FILES ${gem_absolute_file_path}
DESTINATION ${gem_relative_parent_dir}
)
endforeach() endforeach()
# Templates # Templates
@ -626,3 +655,46 @@ function(ly_setup_target_generator)
) )
endfunction() endfunction()
#! ly_add_install_paths: Adds the list of path to copy to the install layout relative to the same folder
# \arg:PATHS - Paths to copy over to the install layout. The DESTINATION sub argument is optional
# The INPUT sub-argument is required
# \arg:BASE_DIRECTORY(Optional) - Absolute path where a relative path from the each input path will be
# based off of. Defaults to LY_ROOT_FOLDER if not supplied
function(ly_add_install_paths)
set(options)
set(oneValueArgs BASE_DIRECTORY)
set(multiValueArgs PATHS)
cmake_parse_arguments(ly_add_install_paths "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(NOT ly_add_install_paths_PATHS)
message(FATAL_ERROR "ly_add_install_paths requires at least one input path to copy to the destination")
endif()
# The default is the "." directory if not supplied
if(NOT ly_add_install_paths_BASE_DIRECTORY)
cmake_path(SET ly_add_install_paths_BASE_DIRECTORY ${LY_ROOT_FOLDER})
endif()
# Separate each path into an INPUT and DESTINATION parameter
set(options)
set(oneValueArgs INPUT DESTINATION)
set(multiValueArgs)
foreach(install_path IN LISTS ly_add_install_paths_PATHS)
string(REPLACE " " ";" install_path ${install_path})
cmake_parse_arguments(install "${options}" "${oneValueArgs}" "${multiValueArgs}" ${install_path})
if(NOT install_DESTINATION)
ly_get_engine_relative_source_dir(${install_INPUT} rel_to_root_input_path
BASE_DIRECTORY ${ly_add_install_paths_BASE_DIRECTORY})
cmake_path(GET rel_to_root_input_path PARENT_PATH install_DESTINATION)
endif()
if(NOT install_DESTINATION)
cmake_path(SET install_DESTINATION .)
endif()
if(IS_DIRECTORY ${install_INPUT})
install(DIRECTORY ${install_INPUT} DESTINATION ${install_DESTINATION})
elseif(EXISTS ${install_INPUT})
install(FILES ${install_INPUT} DESTINATION ${install_DESTINATION})
endif()
endforeach()
endfunction()

@ -8,39 +8,16 @@
include(cmake/Platform/Common/Configurations_common.cmake) include(cmake/Platform/Common/Configurations_common.cmake)
include(cmake/Platform/Common/VisualStudio_common.cmake) include(cmake/Platform/Common/VisualStudio_common.cmake)
set(LY_MSVC_SUPPORTED_GENERATORS if(NOT CMAKE_GENERATOR MATCHES "Visual Studio 1[6-7]")
"Visual Studio 15" message(FATAL_ERROR "Generator ${CMAKE_GENERATOR} not supported")
"Visual Studio 16" endif()
)
set(FOUND_SUPPORTED_GENERATOR)
foreach(supported_generator ${LY_MSVC_SUPPORTED_GENERATORS})
if(CMAKE_GENERATOR MATCHES ${supported_generator})
set(FOUND_SUPPORTED_GENERATOR TRUE)
break()
endif()
endforeach()
# VS2017's checks since it defaults the toolchain and target architecture to x86
if(CMAKE_GENERATOR MATCHES "Visual Studio 15")
if(CMAKE_VS_PLATFORM_NAME AND CMAKE_VS_PLATFORM_NAME STREQUAL "Win32") # VS2017 has Win32 as the default architecture
message(FATAL_ERROR "Win32 architecture not supported, specify \"-A x64\" when invoking cmake")
endif()
if(NOT CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE STREQUAL "x64") # There is at least one library (EditorLib) that make the x86 linker to run out of memory
message(FATAL_ERROR "x86 toolset not supported, specify \"-T host=x64\" when invoking cmake")
endif()
else()
# For the other cases, verify that it wasn't invoked with an unsupported architecture. defaults to x86 architecture
if(SUPPORTED_VS_PLATFORM_NAME_OVERRIDE)
set(SUPPORTED_VS_PLATFORM_NAME ${SUPPORTED_VS_PLATFORM_NAME_OVERRIDE})
else()
set(SUPPORTED_VS_PLATFORM_NAME x64)
endif()
if(CMAKE_VS_PLATFORM_NAME AND NOT CMAKE_VS_PLATFORM_NAME STREQUAL "${SUPPORTED_VS_PLATFORM_NAME}") # Verify that it wasn't invoked with an unsupported target/host architecture. Currently only supports x64/x64
message(FATAL_ERROR "${CMAKE_VS_PLATFORM_NAME} architecture not supported") if(CMAKE_VS_PLATFORM_NAME AND NOT CMAKE_VS_PLATFORM_NAME STREQUAL "x64")
endif() message(FATAL_ERROR "${CMAKE_VS_PLATFORM_NAME} target architecture is not supported, it must be 'x64'")
if(CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE AND NOT CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE STREQUAL "x64") endif()
message(FATAL_ERROR "${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE} toolset not supported") if(CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE AND NOT CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE STREQUAL "x64")
endif() message(FATAL_ERROR "${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE} host toolset is not supported, it must be 'x64'")
endif() endif()
ly_append_configurations_options( ly_append_configurations_options(

@ -5,6 +5,13 @@
# #
# #
if(CMAKE_GENERATOR MATCHES "Visual Studio 16") foreach(conf IN LISTS CMAKE_CONFIGURATION_TYPES)
configure_file("${CMAKE_CURRENT_LIST_DIR}/Directory.Build.props" "${CMAKE_BINARY_DIR}/Directory.Build.props" COPYONLY) if(conf STREQUAL debug)
endif() string(APPEND VCPKG_CONFIGURATION_MAPPING " <VcpkgConfiguration Condition=\"'$(Configuration)' == '${conf}'\">Debug</VcpkgConfiguration>\n")
else()
string(APPEND VCPKG_CONFIGURATION_MAPPING " <VcpkgConfiguration Condition=\"'$(Configuration)' == '${conf}'\">Release</VcpkgConfiguration>\n")
endif()
endforeach()
configure_file("${CMAKE_CURRENT_LIST_DIR}/Directory.Build.props" "${CMAKE_BINARY_DIR}/Directory.Build.props" @ONLY)

@ -15,6 +15,7 @@ function(ly_copy source_file target_directory)
if("${source_file}" IS_NEWER_THAN "${target_directory}/${target_filename}") if("${source_file}" IS_NEWER_THAN "${target_directory}/${target_filename}")
message(STATUS "Copying \"${source_file}\" to \"${target_directory}\"...") message(STATUS "Copying \"${source_file}\" to \"${target_directory}\"...")
file(COPY "${source_file}" DESTINATION "${target_directory}" FILE_PERMISSIONS @LY_COPY_PERMISSIONS@ FOLLOW_SYMLINK_CHAIN) file(COPY "${source_file}" DESTINATION "${target_directory}" FILE_PERMISSIONS @LY_COPY_PERMISSIONS@ FOLLOW_SYMLINK_CHAIN)
file(TOUCH_NOCREATE ${target_directory}/${target_filename})
endif() endif()
endif() endif()
endfunction() endfunction()

@ -125,7 +125,7 @@ function(ly_copy source_file target_directory)
file(LOCK ${target_directory}/${target_filename}.lock GUARD FUNCTION TIMEOUT 300) file(LOCK ${target_directory}/${target_filename}.lock GUARD FUNCTION TIMEOUT 300)
endif() endif()
file(COPY "${source_file}" DESTINATION "${target_directory}" FILE_PERMISSIONS @LY_COPY_PERMISSIONS@ FOLLOW_SYMLINK_CHAIN) file(COPY "${source_file}" DESTINATION "${target_directory}" FILE_PERMISSIONS @LY_COPY_PERMISSIONS@ FOLLOW_SYMLINK_CHAIN)
file(TOUCH ${target_directory}/${target_filename}) file(TOUCH_NOCREATE ${target_directory}/${target_filename})
set(anything_new TRUE PARENT_SCOPE) set(anything_new TRUE PARENT_SCOPE)
endif() endif()
endif() endif()

@ -4,15 +4,15 @@
<Window Width="672" Height="500" HexStyle="100a0000" FontId="2">#(loc.WindowTitle)</Window> <Window Width="672" Height="500" HexStyle="100a0000" FontId="2">#(loc.WindowTitle)</Window>
<!-- header font --> <!-- header font -->
<Font Id="0" Height="-24" Weight="500" Foreground="000000">Open Sans</Font> <Font Id="0" Height="-24" Weight="500" Foreground="000000">Segoe UI</Font>
<!-- body font --> <!-- body font -->
<Font Id="1" Height="-12" Weight="500" Foreground="000000">Open Sans</Font> <Font Id="1" Height="-12" Weight="500" Foreground="000000">Segoe UI</Font>
<!-- special font --> <!-- special font -->
<Font Id="2" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Open Sans</Font> <Font Id="2" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
<!-- throwaway font for button bar --> <!-- throwaway font for button bar -->
<Font Id="3" Height="-12" Weight="500" Foreground="EEEEEE" Background="EEEEEE">Open Sans</Font> <Font Id="3" Height="-12" Weight="500" Foreground="EEEEEE" Background="EEEEEE">Segoe UI</Font>
<!-- special font for install warning title --> <!-- special font for install warning title -->
<Font Id="4" Height="-14" Weight="500" Foreground="000000">Open Sans</Font> <Font Id="4" Height="-14" Weight="500" Foreground="000000">Segoe UI</Font>
<!-- top banner --> <!-- top banner -->
<Image X="28" Y="24" Width="168" Height="64" ImageFile="logo.png" Visible="yes"/> <Image X="28" Y="24" Width="168" Height="64" ImageFile="logo.png" Visible="yes"/>

@ -158,10 +158,6 @@ function(ly_delayed_generate_settings_registry)
message(FATAL_ERROR "Dependency ${gem_target} from ${target} does not exist") message(FATAL_ERROR "Dependency ${gem_target} from ${target} does not exist")
endif() endif()
get_property(has_manually_added_dependencies TARGET ${gem_target} PROPERTY MANUALLY_ADDED_DEPENDENCIES SET)
get_target_property(target_type ${gem_target} TYPE)
ly_get_gem_module_root(gem_module_root ${gem_target}) ly_get_gem_module_root(gem_module_root ${gem_target})
file(RELATIVE_PATH gem_module_root_relative_to_engine_root ${LY_ROOT_FOLDER} ${gem_module_root}) file(RELATIVE_PATH gem_module_root_relative_to_engine_root ${LY_ROOT_FOLDER} ${gem_module_root})
@ -179,7 +175,8 @@ function(ly_delayed_generate_settings_registry)
list(JOIN target_gem_dependencies_names ",\n" target_gem_dependencies_names) list(JOIN target_gem_dependencies_names ",\n" target_gem_dependencies_names)
string(CONFIGURE ${gems_json_template} gem_json @ONLY) string(CONFIGURE ${gems_json_template} gem_json @ONLY)
get_target_property(is_imported ${target} IMPORTED) get_target_property(is_imported ${target} IMPORTED)
if(is_imported) get_target_property(target_type ${target} TYPE)
if(is_imported OR target_type STREQUAL UTILITY)
unset(target_dir) unset(target_dir)
foreach(conf IN LISTS CMAKE_CONFIGURATION_TYPES) foreach(conf IN LISTS CMAKE_CONFIGURATION_TYPES)
string(TOUPPER ${conf} UCONF) string(TOUPPER ${conf} UCONF)

@ -17,6 +17,8 @@ ly_add_target(
@RUNTIME_DEPENDENCIES_PLACEHOLDER@ @RUNTIME_DEPENDENCIES_PLACEHOLDER@
) )
@TARGET_RUN_HELPER@
set(configs @CMAKE_CONFIGURATION_TYPES@) set(configs @CMAKE_CONFIGURATION_TYPES@)
foreach(config ${configs}) foreach(config ${configs})
include("@NAME_PLACEHOLDER@_${config}.cmake" OPTIONAL) include("@NAME_PLACEHOLDER@_${config}.cmake" OPTIONAL)

@ -204,8 +204,6 @@ def CheckoutRepo(boolean disableSubmodules = false) {
if (!fileExists(ENGINE_REPOSITORY_NAME)) { if (!fileExists(ENGINE_REPOSITORY_NAME)) {
palMkdir(ENGINE_REPOSITORY_NAME) palMkdir(ENGINE_REPOSITORY_NAME)
} }
palSh('git lfs uninstall', 'Git LFS Uninstall') // Prevent git from pulling lfs objects during checkout
if(fileExists('.git')) { if(fileExists('.git')) {
// If the repository after checkout is locked, likely we took a snapshot while git was running, // If the repository after checkout is locked, likely we took a snapshot while git was running,
@ -239,13 +237,6 @@ def CheckoutRepo(boolean disableSubmodules = false) {
] ]
} }
// Run lfs in a separate step. Jenkins is unable to load the credentials for the custom LFS endpoint
withCredentials([usernamePassword(credentialsId: "${env.GITHUB_USER}", passwordVariable: 'accesstoken', usernameVariable: 'username')]) {
palSh("git config -f .lfsconfig lfs.url https://${username}:${accesstoken}@${env.LFS_URL}", 'Set credentials', false)
}
palSh('git lfs install', 'Git LFS Install')
palSh('git lfs pull', 'Git LFS Pull')
// CHANGE_ID is used by some scripts to identify uniquely the current change (usually metric jobs) // CHANGE_ID is used by some scripts to identify uniquely the current change (usually metric jobs)
palSh('git rev-parse HEAD > commitid', 'Getting commit id') palSh('git rev-parse HEAD > commitid', 'Getting commit id')
env.CHANGE_ID = readFile file: 'commitid' env.CHANGE_ID = readFile file: 'commitid'
@ -586,13 +577,19 @@ finally {
) )
} }
node('controller') { node('controller') {
step([ if("${currentBuild.currentResult}" == "SUCCESS") {
$class: 'Mailer', emailBody = "${BUILD_URL}\nSuccess!"
notifyEveryUnstableBuild: true, } else {
recipients: emailextrecipients([ buildFailure = tm('${BUILD_FAILURE_ANALYZER}')
emailBody = "${BUILD_URL}\n${buildFailure}!"
}
emailext (
body: "${emailBody}",
subject: "${currentBuild.currentResult}: ${JOB_NAME} - Build # ${BUILD_NUMBER}",
recipientProviders: [
[$class: 'RequesterRecipientProvider'] [$class: 'RequesterRecipientProvider']
]) ]
]) )
} }
} catch(Exception e) { } catch(Exception e) {
} }

@ -4,18 +4,18 @@
# #
import argparse import argparse
import ast
import boto3 import boto3
import datetime import datetime
import urllib.request, urllib.error, urllib.parse import urllib.request, urllib.error, urllib.parse
import os import os
import psutil import psutil
import time import time
import requests
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import traceback from contextlib import contextmanager
import threading
import _thread
DEFAULT_REGION = 'us-west-2' DEFAULT_REGION = 'us-west-2'
DEFAULT_DISK_SIZE = 300 DEFAULT_DISK_SIZE = 300
@ -42,14 +42,18 @@ if os.name == 'nt':
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
kernel32.GetDiskFreeSpaceExW.argtypes = (ctypes.c_wchar_p,) + (PULARGE_INTEGER,) * 3 kernel32.GetDiskFreeSpaceExW.argtypes = (ctypes.c_wchar_p,) + (PULARGE_INTEGER,) * 3
class UsageTuple(collections.namedtuple('UsageTuple', 'total, used, free')): class UsageTuple(collections.namedtuple('UsageTuple', 'total, used, free')):
def __str__(self): def __str__(self):
# Add thousands separator to numbers displayed # Add thousands separator to numbers displayed
return self.__class__.__name__ + '(total={:n}, used={:n}, free={:n})'.format(*self) return self.__class__.__name__ + '(total={:n}, used={:n}, free={:n})'.format(*self)
def is_dir_symlink(path): def is_dir_symlink(path):
FILE_ATTRIBUTE_REPARSE_POINT = 0x0400 FILE_ATTRIBUTE_REPARSE_POINT = 0x0400
return os.path.isdir(path) and (ctypes.windll.kernel32.GetFileAttributesW(str(path)) & FILE_ATTRIBUTE_REPARSE_POINT) return os.path.isdir(path) and (
ctypes.windll.kernel32.GetFileAttributesW(str(path)) & FILE_ATTRIBUTE_REPARSE_POINT)
def get_free_space_mb(path): def get_free_space_mb(path):
if sys.version_info < (3,): # Python 2? if sys.version_info < (3,): # Python 2?
@ -77,16 +81,39 @@ if os.name == 'nt':
used = total.value - free.value used = total.value - free.value
return free.value / 1024 / 1024#for now return free.value / 1024 / 1024 # for now
else: else:
def get_free_space_mb(dirname): def get_free_space_mb(dirname):
st = os.statvfs(dirname) st = os.statvfs(dirname)
return st.f_bavail * st.f_frsize / 1024 / 1024 return st.f_bavail * st.f_frsize / 1024 / 1024
def error(message): def error(message):
print(message) print(message)
exit(1) exit(1)
@contextmanager
def timeout(duration, timeout_message):
timer = threading.Timer(duration, lambda: _thread.interrupt_main())
timer.start()
try:
yield
except KeyboardInterrupt:
print(timeout_message)
raise TimeoutError
finally:
# If the action ends in specified time, timer is canceled
timer.cancel()
def print_drives():
if os.name == 'nt':
drives_before = win32api.GetLogicalDriveStrings()
drives_before = drives_before.split('\000')[:-1]
print(drives_before)
def parse_args(): def parse_args():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('-a', '--action', dest="action", help="Action (mount|unmount|delete)") parser.add_argument('-a', '--action', dest="action", help="Action (mount|unmount|delete)")
@ -96,8 +123,10 @@ def parse_args():
parser.add_argument('-b', '--branch', dest="branch", help="Branch") parser.add_argument('-b', '--branch', dest="branch", help="Branch")
parser.add_argument('-plat', '--platform', dest="platform", help="Platform") parser.add_argument('-plat', '--platform', dest="platform", help="Platform")
parser.add_argument('-c', '--build_type', dest="build_type", help="Build type") parser.add_argument('-c', '--build_type', dest="build_type", help="Build type")
parser.add_argument('-ds', '--disk_size', dest="disk_size", help="Disk size in Gigabytes (defaults to {})".format(DEFAULT_DISK_SIZE), default=DEFAULT_DISK_SIZE) parser.add_argument('-ds', '--disk_size', dest="disk_size",
parser.add_argument('-dt', '--disk_type', dest="disk_type", help="Disk type (defaults to {})".format(DEFAULT_DISK_TYPE), default=DEFAULT_DISK_TYPE) help=f"Disk size in Gigabytes (defaults to {DEFAULT_DISK_SIZE})", default=DEFAULT_DISK_SIZE)
parser.add_argument('-dt', '--disk_type', dest="disk_type", help=f"Disk type (defaults to {DEFAULT_DISK_TYPE})",
default=DEFAULT_DISK_TYPE)
args = parser.parse_args() args = parser.parse_args()
# Input validation # Input validation
@ -117,19 +146,30 @@ def parse_args():
error('No platform specified') error('No platform specified')
if args.build_type is None: if args.build_type is None:
error('No build_type specified') error('No build_type specified')
return args return args
def get_mount_name(repository_name, project, pipeline, branch, platform, build_type): def get_mount_name(repository_name, project, pipeline, branch, platform, build_type):
mount_name = "{}_{}_{}_{}_{}_{}".format(repository_name, project, pipeline, branch, platform, build_type) mount_name = f"{repository_name}_{project}_{pipeline}_{branch}_{platform}_{build_type}"
mount_name = mount_name.replace('/','_').replace('\\','_') mount_name = mount_name.replace('/', '_').replace('\\', '_')
return mount_name return mount_name
def get_pipeline_and_branch(pipeline, branch): def get_pipeline_and_branch(pipeline, branch):
pipeline_and_branch = "{}_{}".format(pipeline, branch) pipeline_and_branch = f"{pipeline}_{branch}"
pipeline_and_branch = pipeline_and_branch.replace('/','_').replace('\\','_') pipeline_and_branch = pipeline_and_branch.replace('/', '_').replace('\\', '_')
return pipeline_and_branch return pipeline_and_branch
def get_region_name():
session = boto3.session.Session()
region = session.region_name
if region is None:
region = DEFAULT_REGION
return region
def get_ec2_client(region): def get_ec2_client(region):
client = boto3.client('ec2', region_name=region) client = boto3.client('ec2', region_name=region)
return client return client
@ -140,48 +180,51 @@ def get_ec2_instance_id():
instance_id = urllib.request.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read() instance_id = urllib.request.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read()
return instance_id.decode("utf-8") return instance_id.decode("utf-8")
except Exception as e: except Exception as e:
print(e.message) print(e)
error('No EC2 metadata! Check if you are running this script on an EC2 instance.') error('No EC2 metadata! Check if you are running this script on an EC2 instance.')
def get_availability_zone(): def get_availability_zone():
try: try:
availability_zone = urllib.request.urlopen('http://169.254.169.254/latest/meta-data/placement/availability-zone').read() availability_zone = urllib.request.urlopen(
'http://169.254.169.254/latest/meta-data/placement/availability-zone').read()
return availability_zone.decode("utf-8") return availability_zone.decode("utf-8")
except Exception as e: except Exception as e:
print(e.message) print(e)
error('No EC2 metadata! Check if you are running this script on an EC2 instance.') error('No EC2 metadata! Check if you are running this script on an EC2 instance.')
def kill_processes(workspace='/dev/'): def kill_processes(workspace='/dev/'):
''' """
Kills all processes that have open file paths associated with the workspace. Kills all processes that have open file paths associated with the workspace.
Uses PSUtil for cross-platform compatibility Uses PSUtil for cross-platform compatibility
''' """
print('Checking for any stuck processes...') print('Checking for any stuck processes...')
for proc in psutil.process_iter(): for proc in psutil.process_iter():
try: try:
if workspace in str(proc.open_files()): if workspace in str(proc.open_files()):
print("{} has open files in {}. Terminating".format(proc.name(), proc.open_files())) print(f"{proc.name()} has open files in {proc.open_files()}. Terminating")
proc.kill() proc.kill()
time.sleep(1) # Just to make sure a parent process has time to close time.sleep(1) # Just to make sure a parent process has time to close
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue continue
def delete_volume(ec2_client, volume_id): def delete_volume(ec2_client, volume_id):
response = ec2_client.delete_volume(VolumeId=volume_id) response = ec2_client.delete_volume(VolumeId=volume_id)
print('Volume {} deleted'.format(volume_id)) print(f'Volume {volume_id} deleted')
def find_snapshot_id(ec2_client, repository_name, project, pipeline, platform, build_type, disk_size): def find_snapshot_id(ec2_client, repository_name, project, pipeline, platform, build_type, disk_size):
mount_name = get_mount_name(repository_name, project, pipeline, 'stabilization_2106', platform, build_type) # we take snapshots out of stabilization_2106 mount_name = get_mount_name(repository_name, project, pipeline, 'stabilization_2106', platform,
response = ec2_client.describe_snapshots(Filters= [{ build_type) # we take snapshots out of stabilization_2106
response = ec2_client.describe_snapshots(Filters=[{
'Name': 'tag:Name', 'Values': [mount_name] 'Name': 'tag:Name', 'Values': [mount_name]
}]) }])
snapshot_id = None snapshot_id = None
if 'Snapshots' in response and len(response['Snapshots']) > 0: if 'Snapshots' in response and len(response['Snapshots']) > 0:
snapshot_start_time_max = None # find the latest snapshot snapshot_start_time_max = None # find the latest snapshot
for snapshot in response['Snapshots']: for snapshot in response['Snapshots']:
if snapshot['State'] == 'completed' and snapshot['VolumeSize'] == disk_size: if snapshot['State'] == 'completed' and snapshot['VolumeSize'] == disk_size:
snapshot_start_time = snapshot['StartTime'] snapshot_start_time = snapshot['StartTime']
@ -190,28 +233,33 @@ def find_snapshot_id(ec2_client, repository_name, project, pipeline, platform, b
snapshot_id = snapshot['SnapshotId'] snapshot_id = snapshot['SnapshotId']
return snapshot_id return snapshot_id
def create_volume(ec2_client, availability_zone, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type):
# The actual EBS default calculation for IOps is a floating point number, the closest approxmiation is 4x of the disk size for simplicity def create_volume(ec2_client, availability_zone, repository_name, project, pipeline, branch, platform, build_type,
disk_size, disk_type):
mount_name = get_mount_name(repository_name, project, pipeline, branch, platform, build_type) mount_name = get_mount_name(repository_name, project, pipeline, branch, platform, build_type)
pipeline_and_branch = get_pipeline_and_branch(pipeline, branch) pipeline_and_branch = get_pipeline_and_branch(pipeline, branch)
parameters = dict( parameters = dict(
AvailabilityZone = availability_zone, AvailabilityZone=availability_zone,
VolumeType=disk_type, VolumeType=disk_type,
TagSpecifications= [{ TagSpecifications=[{
'ResourceType': 'volume', 'ResourceType': 'volume',
'Tags': [ 'Tags': [
{ 'Key': 'Name', 'Value': mount_name }, {'Key': 'Name', 'Value': mount_name},
{ 'Key': 'RepositoryName', 'Value': repository_name}, {'Key': 'RepositoryName', 'Value': repository_name},
{ 'Key': 'Project', 'Value': project }, {'Key': 'Project', 'Value': project},
{ 'Key': 'Pipeline', 'Value': pipeline }, {'Key': 'Pipeline', 'Value': pipeline},
{ 'Key': 'BranchName', 'Value': branch }, {'Key': 'BranchName', 'Value': branch},
{ 'Key': 'Platform', 'Value': platform }, {'Key': 'Platform', 'Value': platform},
{ 'Key': 'BuildType', 'Value': build_type }, {'Key': 'BuildType', 'Value': build_type},
{ 'Key': 'PipelineAndBranch', 'Value': pipeline_and_branch }, # used so the snapshoting easily identifies which volumes to snapshot # Used so the snapshoting easily identifies which volumes to snapshot
{'Key': 'PipelineAndBranch', 'Value': pipeline_and_branch},
] ]
}] }]
) )
if 'io1' in disk_type.lower(): # The actual EBS default calculation for IOps is a floating point number,
# the closest approxmiation is 4x of the disk size for simplicity
if 'io1' in disk_type.lower():
parameters['Iops'] = (4 * disk_size) parameters['Iops'] = (4 * disk_size)
snapshot_id = find_snapshot_id(ec2_client, repository_name, project, pipeline, platform, build_type, disk_size) snapshot_id = find_snapshot_id(ec2_client, repository_name, project, pipeline, platform, build_type, disk_size)
@ -230,16 +278,17 @@ def create_volume(ec2_client, availability_zone, repository_name, project, pipel
time.sleep(1) time.sleep(1)
response = ec2_client.describe_volumes(VolumeIds=[volume_id, ]) response = ec2_client.describe_volumes(VolumeIds=[volume_id, ])
while (response['Volumes'][0]['State'] != 'available'): with timeout(DEFAULT_TIMEOUT, 'ERROR: Timeout reached trying to create EBS.'):
time.sleep(1) while response['Volumes'][0]['State'] != 'available':
response = ec2_client.describe_volumes(VolumeIds=[volume_id, ]) time.sleep(1)
response = ec2_client.describe_volumes(VolumeIds=[volume_id, ])
print(("Volume {} created\n\tSnapshot: {}\n\tRepository {}\n\tProject {}\n\tPipeline {}\n\tBranch {}\n\tPlatform: {}\n\tBuild type: {}" print(f"Volume {volume_id} created\n\tSnapshot: {snapshot_id}\n\tRepository {repository_name}\n\t"
.format(volume_id, snapshot_id, repository_name, project, pipeline, branch, platform, build_type))) f"Project {project}\n\tPipeline {pipeline}\n\tBranch {branch}\n\tPlatform: {platform}\n\tBuild type: {build_type}")
return volume_id, created return volume_id, created
def mount_volume(created): def mount_volume_to_device(created):
print('Mounting volume...') print('Mounting volume...')
if os.name == 'nt': if os.name == 'nt':
f = tempfile.NamedTemporaryFile(delete=False) f = tempfile.NamedTemporaryFile(delete=False)
@ -247,7 +296,7 @@ def mount_volume(created):
select disk 1 select disk 1
online disk online disk
attribute disk clear readonly attribute disk clear readonly
""".encode('utf-8')) # assume disk # for now """.encode('utf-8')) # assume disk # for now
if created: if created:
print('Creating filesystem on new volume') print('Creating filesystem on new volume')
@ -259,18 +308,12 @@ def mount_volume(created):
""".encode('utf-8')) """.encode('utf-8'))
f.close() f.close()
subprocess.call(['diskpart', '/s', f.name]) subprocess.call(['diskpart', '/s', f.name])
time.sleep(5) time.sleep(5)
drives_after = win32api.GetLogicalDriveStrings() print_drives()
drives_after = drives_after.split('\000')[:-1]
print(drives_after)
#drive_letter = next(item for item in drives_after if item not in drives_before)
drive_letter = MOUNT_PATH
os.unlink(f.name) os.unlink(f.name)
@ -283,8 +326,8 @@ def mount_volume(created):
subprocess.call(['mount', '/dev/xvdf', MOUNT_PATH]) subprocess.call(['mount', '/dev/xvdf', MOUNT_PATH])
def attach_volume(volume, volume_id, instance_id, timeout=DEFAULT_TIMEOUT): def attach_volume_to_ec2_instance(volume, volume_id, instance_id, timeout_duration=DEFAULT_TIMEOUT):
print('Attaching volume {} to instance {}'.format(volume_id, instance_id)) print(f'Attaching volume {volume_id} to instance {instance_id}')
volume.attach_to_instance(Device='xvdf', volume.attach_to_instance(Device='xvdf',
InstanceId=instance_id, InstanceId=instance_id,
VolumeId=volume_id) VolumeId=volume_id)
@ -292,13 +335,10 @@ def attach_volume(volume, volume_id, instance_id, timeout=DEFAULT_TIMEOUT):
time.sleep(2) time.sleep(2)
# reload the volume just in case # reload the volume just in case
volume.load() volume.load()
timeout_init = time.clock() with timeout(timeout_duration, 'ERROR: Timeout reached trying to mount EBS.'):
while (len(volume.attachments) and volume.attachments[0]['State'] != 'attached'): while len(volume.attachments) and volume.attachments[0]['State'] != 'attached':
time.sleep(1) time.sleep(1)
volume.load() volume.load()
if (time.clock() - timeout_init) > timeout:
print('ERROR: Timeout reached trying to mount EBS')
exit(1)
volume.create_tags( volume.create_tags(
Tags=[ Tags=[
{ {
@ -307,11 +347,11 @@ def attach_volume(volume, volume_id, instance_id, timeout=DEFAULT_TIMEOUT):
}, },
] ]
) )
print('Volume {} has been attached to instance {}'.format(volume_id, instance_id)) print(f'Volume {volume_id} has been attached to instance {instance_id}')
def unmount_volume(): def unmount_volume_from_device():
print('Umounting volume...') print('Unmounting EBS volume from device...')
if os.name == 'nt': if os.name == 'nt':
kill_processes(MOUNT_PATH + 'workspace') kill_processes(MOUNT_PATH + 'workspace')
f = tempfile.NamedTemporaryFile(delete=False) f = tempfile.NamedTemporaryFile(delete=False)
@ -327,50 +367,31 @@ def unmount_volume():
subprocess.call(['umount', '-f', MOUNT_PATH]) subprocess.call(['umount', '-f', MOUNT_PATH])
def detach_volume(volume, ec2_instance_id, force, timeout=DEFAULT_TIMEOUT): def detach_volume_from_ec2_instance(volume, ec2_instance_id, force, timeout_duration=DEFAULT_TIMEOUT):
print('Detaching volume {} from instance {}'.format(volume.volume_id, ec2_instance_id)) print(f'Detaching volume {volume.volume_id} from instance {ec2_instance_id}')
volume.detach_from_instance(Device='xvdf', volume.detach_from_instance(Device='xvdf',
Force=force, Force=force,
InstanceId=ec2_instance_id, InstanceId=ec2_instance_id,
VolumeId=volume.volume_id) VolumeId=volume.volume_id)
timeout_init = time.clock() try:
while len(volume.attachments) and volume.attachments[0]['State'] != 'detached': with timeout(timeout_duration, 'ERROR: Timeout reached trying to unmount EBS.'):
time.sleep(1) while len(volume.attachments) and volume.attachments[0]['State'] != 'detached':
volume.load() time.sleep(1)
if (time.clock() - timeout_init) > timeout: volume.load()
print('ERROR: Timeout reached trying to unmount EBS.') except TimeoutError:
volume.detach_from_instance(Device='xvdf',Force=True,InstanceId=ec2_instance_id,VolumeId=volume.volume_id) print('Force detaching EBS.')
exit(1) volume.detach_from_instance(Device='xvdf', Force=True, InstanceId=ec2_instance_id, VolumeId=volume.volume_id)
print('Volume {} has been detached from instance {}'.format(volume.volume_id, ec2_instance_id)) print(f'Volume {volume.volume_id} has been detached from instance {ec2_instance_id}')
volume.load() volume.load()
if len(volume.attachments): if len(volume.attachments):
print('Volume still has attachments') print('Volume still has attachments')
for attachment in volume.attachments: for attachment in volume.attachments:
print('Volume {} {} to instance {}'.format(attachment['VolumeId'], attachment['State'], attachment['InstanceId'])) print(f"Volume {attachment['VolumeId']} {attachment['State']} to instance {attachment['InstanceId']}")
def attach_ebs_and_create_partition_with_retry(volume, volume_id, ec2_instance_id, created):
attach_volume(volume, volume_id, ec2_instance_id)
mount_volume(created)
attempt = 1
while attempt <= MAX_EBS_MOUNTING_ATTEMPT:
if os.name == 'nt':
drives_after = win32api.GetLogicalDriveStrings()
drives_after = drives_after.split('\000')[:-1]
if MOUNT_PATH not in drives_after:
print('Disk partitioning failed, retrying...')
unmount_volume()
detach_volume(volume, ec2_instance_id, False)
attach_volume(volume, volume_id, ec2_instance_id)
mount_volume(created)
attempt += 1
def mount_ebs(repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type): def mount_ebs(repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type):
session = boto3.session.Session() region = get_region_name()
region = session.region_name
if region is None:
region = DEFAULT_REGION
ec2_client = get_ec2_client(region) ec2_client = get_ec2_client(region)
ec2_instance_id = get_ec2_instance_id() ec2_instance_id = get_ec2_instance_id()
ec2_availability_zone = get_availability_zone() ec2_availability_zone = get_availability_zone()
@ -379,70 +400,70 @@ def mount_ebs(repository_name, project, pipeline, branch, platform, build_type,
for volume in ec2_instance.volumes.all(): for volume in ec2_instance.volumes.all():
for attachment in volume.attachments: for attachment in volume.attachments:
print('attachment device: {}'.format(attachment['Device'])) print(f"attachment device: {attachment['Device']}")
if 'xvdf' in attachment['Device'] and attachment['State'] != 'detached': if 'xvdf' in attachment['Device'] and attachment['State'] != 'detached':
print('A device is already attached to xvdf. This likely means a previous build failed to detach its ' \ print('A device is already attached to xvdf. This likely means a previous build failed to detach its '
'build volume. This volume is considered orphaned and will be detached from this instance.') 'build volume. This volume is considered orphaned and will be detached from this instance.')
unmount_volume() unmount_volume_from_device()
detach_volume(volume, ec2_instance_id, False) # Force unmounts should not be used, as that will cause the EBS block device driver to fail the remount detach_volume_from_ec2_instance(volume, ec2_instance_id,
False) # Force unmounts should not be used, as that will cause the EBS block device driver to fail the remount
mount_name = get_mount_name(repository_name, project, pipeline, branch, platform, build_type) mount_name = get_mount_name(repository_name, project, pipeline, branch, platform, build_type)
response = ec2_client.describe_volumes(Filters=[{ response = ec2_client.describe_volumes(Filters=[{
'Name': 'tag:Name', 'Values': [mount_name] 'Name': 'tag:Name', 'Values': [mount_name]
}]) }])
created = False created = False
if 'Volumes' in response and not len(response['Volumes']): if 'Volumes' in response and not len(response['Volumes']):
print('Volume for {} doesn\'t exist creating it...'.format(mount_name)) print(f'Volume for {mount_name} doesn\'t exist creating it...')
# volume doesn't exist, create it # volume doesn't exist, create it
volume_id, created = create_volume(ec2_client, ec2_availability_zone, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) volume_id, created = create_volume(ec2_client, ec2_availability_zone, repository_name, project, pipeline,
branch, platform, build_type, disk_size, disk_type)
else: else:
volume = response['Volumes'][0] volume = response['Volumes'][0]
volume_id = volume['VolumeId'] volume_id = volume['VolumeId']
print('Current volume {} is a {} GB {}'.format(volume_id, volume['Size'], volume['VolumeType'])) print(f"Current volume {volume_id} is a {volume['Size']} GB {volume['VolumeType']}")
if (volume['Size'] != disk_size or volume['VolumeType'] != disk_type): if volume['Size'] != disk_size or volume['VolumeType'] != disk_type:
print('Override disk attributes does not match the existing volume, deleting {} and replacing the volume'.format(volume_id)) print(
f'Override disk attributes does not match the existing volume, deleting {volume_id} and replacing the volume')
delete_volume(ec2_client, volume_id) delete_volume(ec2_client, volume_id)
volume_id, created = create_volume(ec2_client, ec2_availability_zone, repository_name, project, pipeline, branch, platform, build_type, disk_size, disk_type) volume_id, created = create_volume(ec2_client, ec2_availability_zone, repository_name, project, pipeline,
branch, platform, build_type, disk_size, disk_type)
if len(volume['Attachments']): if len(volume['Attachments']):
# this is bad we shouldn't be attached, we should have detached at the end of a build # this is bad we shouldn't be attached, we should have detached at the end of a build
attachment = volume['Attachments'][0] attachment = volume['Attachments'][0]
print(('Volume already has attachment {}, detaching...'.format(attachment))) print(f'Volume already has attachment {attachment}, detaching...')
detach_volume(ec2_resource.Volume(volume_id), attachment['InstanceId'], True) detach_volume_from_ec2_instance(ec2_resource.Volume(volume_id), attachment['InstanceId'], True)
volume = ec2_resource.Volume(volume_id) volume = ec2_resource.Volume(volume_id)
if os.name == 'nt': print_drives()
drives_before = win32api.GetLogicalDriveStrings() attach_volume_to_ec2_instance(volume, volume_id, ec2_instance_id)
drives_before = drives_before.split('\000')[:-1] mount_volume_to_device(created)
print_drives()
print(drives_before)
attach_ebs_and_create_partition_with_retry(volume, volume_id, ec2_instance_id, created)
free_space_mb = get_free_space_mb(MOUNT_PATH) free_space_mb = get_free_space_mb(MOUNT_PATH)
print('Free disk space {}MB'.format(free_space_mb)) print(f'Free disk space {free_space_mb}MB')
if free_space_mb < LOW_EBS_DISK_SPACE_LIMIT: if free_space_mb < LOW_EBS_DISK_SPACE_LIMIT:
print('Volume is running below EBS free disk space treshhold {}MB. Recreating volume and running clean build.'.format(LOW_EBS_DISK_SPACE_LIMIT)) print(f'Volume is running below EBS free disk space treshhold {LOW_EBS_DISK_SPACE_LIMIT}MB. Recreating volume and running clean build.')
unmount_volume() unmount_volume_from_device()
detach_volume(volume, ec2_instance_id, False) detach_volume_from_ec2_instance(volume, ec2_instance_id, False)
delete_volume(ec2_client, volume_id) delete_volume(ec2_client, volume_id)
new_disk_size = int(volume.size * 1.25) new_disk_size = int(volume.size * 1.25)
if new_disk_size > MAX_EBS_DISK_SIZE: if new_disk_size > MAX_EBS_DISK_SIZE:
print('Error: EBS disk size reached to the allowed maximum disk size {}MB, please contact ly-infra@ and ly-build@ to investigate.'.format(MAX_EBS_DISK_SIZE)) print(f'Error: EBS disk size reached to the allowed maximum disk size {MAX_EBS_DISK_SIZE}MB, please contact ly-infra@ and ly-build@ to investigate.')
exit(1) exit(1)
print('Recreating the EBS with disk size {}'.format(new_disk_size)) print(f'Recreating the EBS with disk size {new_disk_size}')
volume_id, created = create_volume(ec2_client, ec2_availability_zone, repository_name, project, pipeline, branch, platform, build_type, new_disk_size, disk_type) volume_id, created = create_volume(ec2_client, ec2_availability_zone, repository_name, project, pipeline,
branch, platform, build_type, new_disk_size, disk_type)
volume = ec2_resource.Volume(volume_id) volume = ec2_resource.Volume(volume_id)
attach_ebs_and_create_partition_with_retry(volume, volume_id, ec2_instance_id, created) attach_volume_to_ec2_instance(volume, volume_id, ec2_instance_id)
mount_volume_to_device(created)
def unmount_ebs(): def unmount_ebs():
session = boto3.session.Session() region = get_region_name()
region = session.region_name
if region is None:
region = DEFAULT_REGION
ec2_client = get_ec2_client(region)
ec2_instance_id = get_ec2_instance_id() ec2_instance_id = get_ec2_instance_id()
ec2_resource = boto3.resource('ec2', region_name=region) ec2_resource = boto3.resource('ec2', region_name=region)
ec2_instance = ec2_resource.Instance(ec2_instance_id) ec2_instance = ec2_resource.Instance(ec2_instance_id)
@ -454,7 +475,7 @@ def unmount_ebs():
for attached_volume in ec2_instance.volumes.all(): for attached_volume in ec2_instance.volumes.all():
for attachment in attached_volume.attachments: for attachment in attached_volume.attachments:
print('attachment device: {}'.format(attachment['Device'])) print(f"attachment device: {attachment['Device']}")
if attachment['Device'] == 'xvdf': if attachment['Device'] == 'xvdf':
volume = attached_volume volume = attached_volume
@ -462,24 +483,18 @@ def unmount_ebs():
# volume is not mounted # volume is not mounted
print('Volume is not mounted') print('Volume is not mounted')
else: else:
unmount_volume() unmount_volume_from_device()
detach_volume(volume, ec2_instance_id, False) detach_volume_from_ec2_instance(volume, ec2_instance_id, False)
def delete_ebs(repository_name, project, pipeline, branch, platform, build_type): def delete_ebs(repository_name, project, pipeline, branch, platform, build_type):
unmount_ebs() unmount_ebs()
region = get_region_name()
session = boto3.session.Session()
region = session.region_name
if region is None:
region = DEFAULT_REGION
ec2_client = get_ec2_client(region) ec2_client = get_ec2_client(region)
ec2_instance_id = get_ec2_instance_id()
ec2_resource = boto3.resource('ec2', region_name=region)
ec2_instance = ec2_resource.Instance(ec2_instance_id)
mount_name = get_mount_name(repository_name, project, pipeline, branch, platform, build_type) mount_name = get_mount_name(repository_name, project, pipeline, branch, platform, build_type)
response = ec2_client.describe_volumes(Filters=[ response = ec2_client.describe_volumes(Filters=[
{ 'Name': 'tag:Name', 'Values': [mount_name] } {'Name': 'tag:Name', 'Values': [mount_name]}
]) ])
if 'Volumes' in response and len(response['Volumes']): if 'Volumes' in response and len(response['Volumes']):
@ -496,7 +511,8 @@ def main(action, repository_name, project, pipeline, branch, platform, build_typ
elif action == 'delete': elif action == 'delete':
delete_ebs(repository_name, project, pipeline, branch, platform, build_type) delete_ebs(repository_name, project, pipeline, branch, platform, build_type)
if __name__ == "__main__": if __name__ == "__main__":
args = parse_args() args = parse_args()
ret = main(args.action, args.repository_name, args.project, args.pipeline, args.branch, args.platform, args.build_type, args.disk_size, args.disk_type) main(args.action, args.repository_name, args.project, args.pipeline, args.branch, args.platform,
sys.exit(ret) args.build_type, args.disk_size, args.disk_type)

@ -0,0 +1,129 @@
#
# Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
import argparse
import fnmatch
import json
import os
import pathlib
import re
import sys
class LicenseScanner:
"""Class to contain license scanner.
Scans source tree for license files using provided filename patterns and generates a file
with the contents of all the licenses.
:param config_file: Config file with license patterns and scanner settings
"""
DEFAULT_CONFIG_FILE = 'scanner_config.json'
def __init__(self, config_file=None):
self.config_file = config_file
self.config_data = self._load_config()
self.license_regex = self._load_license_regex()
def _load_config(self):
"""Load config from the provided file. Sets default file if one is not provided."""
if self.config_file is None:
script_directory = os.path.dirname(os.path.abspath(__file__)) # Default file expected in same dir as script
self.config_file = os.path.join(script_directory, self.DEFAULT_CONFIG_FILE)
try:
with open(self.config_file) as f:
return json.load(f)
except FileNotFoundError:
print('Config file cannot be found')
raise
def _load_license_regex(self):
"""Returns regex object with case-insensitive matching from the list of filename patterns."""
regex_patterns = []
for pattern in self.config_data['license_patterns']:
regex_patterns.append(fnmatch.translate(pattern))
return re.compile('|'.join(regex_patterns), re.IGNORECASE)
def scan(self, path=os.curdir):
"""Scan directory tree for filenames matching license_regex.
:param path: Path of the directory to run scanner
:return: Package paths and their corresponding license file contents
:rtype: dict
"""
licenses = 0
license_files = {}
for dirpath, dirnames, filenames in os.walk(path):
for file in filenames:
if self.license_regex.match(file):
license_file_content = self._get_license_file_contents(os.path.join(dirpath, file))
rel_dirpath = os.path.relpath(dirpath, path) # Limit path inside scanned directory
license_files[rel_dirpath] = license_file_content
licenses += 1
print(f'License file: {os.path.join(dirpath, file)}')
# Remove directories that should not be scanned
for dir in self.config_data['excluded_directories']:
if dir in dirnames:
dirnames.remove(dir)
print(f'{licenses} license files found.')
return license_files
def _get_license_file_contents(self, filepath):
try:
with open(filepath, encoding='utf8') as f:
return f.read()
except UnicodeDecodeError:
print(f'Unable to read license file: {filepath}')
pass
def create_license_file(self, licenses, filepath='NOTICES.txt'):
"""Creates file with all the provided license file contents.
:param licenses: Dict with package paths and their corresponding license file contents
:param filepath: Path to write the file
"""
package_separator = '------------------------------------'
with open(filepath, 'w', encoding='utf8') as f:
for directory, license in licenses.items():
license_output = '\n\n'.join([
f'{package_separator}',
f'Package path: {directory}',
'License:',
f'{license}\n'
])
f.write(license_output)
return None
def parse_args():
parser = argparse.ArgumentParser(
description='Script to run LicenseScanner and generate license file')
parser.add_argument('--config-file', '-c', type=pathlib.Path, help='Config file for LicenseScanner')
parser.add_argument('--license-file-path', '-l', type=pathlib.Path, help='Create license file in the provided path')
parser.add_argument('--scan-path', '-s', default=os.curdir, type=pathlib.Path, help='Path to scan')
return parser.parse_args()
def main():
try:
args = parse_args()
ls = LicenseScanner(args.config_file)
licenses = ls.scan(args.scan_path)
if args.license_file_path:
ls.create_license_file(licenses, args.license_file_path)
except FileNotFoundError as e:
print(f'Type: {type(e).__name__}, Error: {e}')
return 1
if __name__ == '__main__':
sys.exit(main())

@ -0,0 +1,12 @@
{
"excluded_directories": [
".git",
".venv",
"build",
"license_scanner"
],
"license_patterns": [
"LICENSE*",
"COPYING*"
]
}
Loading…
Cancel
Save