Fix project creation (#1445)

* Add ability to change project name

* Fixed several issues where path types were changed

* Added PythonBindings CreateProject unit test

* Fix python warning format

* Validate new project name in CLI

* Fix issue creating pathview on linux

* Use better testing macros

* Refactored the unit_test_engine_template.py test to actually test
against the current engine_template.py commands

The commands of create-template, create-from-template, create-project
and create-gem is now being validated.

Registered the unit_test_engine_template.py script with CTest in the smoke test
suite so that it runs in Automated Review

Fixed issues in the engine_template.py script where the template_restricted_path parameter was required in the create_project and create_gem functions

Co-authored-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com>
main
Alex Peterson 5 years ago committed by GitHub
parent 64533431d8
commit aa885e5d0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -83,6 +83,8 @@ namespace O3DE::ProjectManager
{ {
ProjectInfo projectInfo; ProjectInfo projectInfo;
projectInfo.m_projectName = m_projectName->lineEdit()->text(); projectInfo.m_projectName = m_projectName->lineEdit()->text();
// currently we don't have separate fields for changing the project name and display name
projectInfo.m_displayName = projectInfo.m_projectName;
projectInfo.m_path = m_projectPath->lineEdit()->text(); projectInfo.m_path = m_projectPath->lineEdit()->text();
return projectInfo; return projectInfo;
} }

@ -52,8 +52,10 @@ namespace Platform
} // namespace Platform } // namespace Platform
#define Py_To_String(obj) obj.cast<std::string>().c_str() #define Py_To_String(obj) pybind11::str(obj).cast<std::string>().c_str()
#define Py_To_String_Optional(dict, key, default_string) dict.contains(key) ? Py_To_String(dict[key]) : default_string #define Py_To_String_Optional(dict, key, default_string) dict.contains(key) ? Py_To_String(dict[key]) : default_string
#define QString_To_Py_String(value) pybind11::str(value.toStdString())
#define QString_To_Py_Path(value) m_pathlib.attr("Path")(value.toStdString())
namespace RedirectOutput namespace RedirectOutput
{ {
@ -298,6 +300,7 @@ namespace O3DE::ProjectManager
m_enableGemProject = pybind11::module::import("o3de.enable_gem"); m_enableGemProject = pybind11::module::import("o3de.enable_gem");
m_disableGemProject = pybind11::module::import("o3de.disable_gem"); m_disableGemProject = pybind11::module::import("o3de.disable_gem");
m_editProjectProperties = pybind11::module::import("o3de.project_properties"); m_editProjectProperties = pybind11::module::import("o3de.project_properties");
m_pathlib = pybind11::module::import("pathlib");
// make sure the engine is registered // make sure the engine is registered
RegisterThisEngine(); RegisterThisEngine();
@ -346,7 +349,7 @@ namespace O3DE::ProjectManager
} }
} }
auto result = m_register.attr("register")(m_enginePath.c_str()); auto result = m_register.attr("register")(QString_To_Py_Path(QString(m_enginePath.c_str())));
registrationResult = (result.cast<int>() == 0); registrationResult = (result.cast<int>() == 0);
}); });
@ -388,7 +391,7 @@ namespace O3DE::ProjectManager
{ {
EngineInfo engineInfo; EngineInfo engineInfo;
bool result = ExecuteWithLock([&] { bool result = ExecuteWithLock([&] {
pybind11::str enginePath = m_manifest.attr("get_this_engine_path")(); auto enginePath = m_manifest.attr("get_this_engine_path")();
auto o3deData = m_manifest.attr("load_o3de_manifest")(); auto o3deData = m_manifest.attr("load_o3de_manifest")();
if (pybind11::isinstance<pybind11::dict>(o3deData)) if (pybind11::isinstance<pybind11::dict>(o3deData))
@ -399,7 +402,7 @@ namespace O3DE::ProjectManager
engineInfo.m_defaultRestrictedFolder = Py_To_String(o3deData["default_restricted_folder"]); engineInfo.m_defaultRestrictedFolder = Py_To_String(o3deData["default_restricted_folder"]);
engineInfo.m_defaultTemplatesFolder = Py_To_String(o3deData["default_templates_folder"]); engineInfo.m_defaultTemplatesFolder = Py_To_String(o3deData["default_templates_folder"]);
pybind11::str defaultThirdPartyFolder = m_manifest.attr("get_o3de_third_party_folder")(); auto defaultThirdPartyFolder = m_manifest.attr("get_o3de_third_party_folder")();
engineInfo.m_thirdPartyPath = Py_To_String_Optional(o3deData,"default_third_party_folder", Py_To_String(defaultThirdPartyFolder)); engineInfo.m_thirdPartyPath = Py_To_String_Optional(o3deData,"default_third_party_folder", Py_To_String(defaultThirdPartyFolder));
} }
@ -433,14 +436,8 @@ namespace O3DE::ProjectManager
bool PythonBindings::SetEngineInfo(const EngineInfo& engineInfo) bool PythonBindings::SetEngineInfo(const EngineInfo& engineInfo)
{ {
bool result = ExecuteWithLock([&] { bool result = ExecuteWithLock([&] {
pybind11::str enginePath = engineInfo.m_path.toStdString();
pybind11::str defaultProjectsFolder = engineInfo.m_defaultProjectsFolder.toStdString();
pybind11::str defaultGemsFolder = engineInfo.m_defaultGemsFolder.toStdString();
pybind11::str defaultTemplatesFolder = engineInfo.m_defaultTemplatesFolder.toStdString();
pybind11::str defaultThirdPartyFolder = engineInfo.m_thirdPartyPath.toStdString();
auto registrationResult = m_register.attr("register")( auto registrationResult = m_register.attr("register")(
enginePath, // engine_path QString_To_Py_Path(engineInfo.m_path),
pybind11::none(), // project_path pybind11::none(), // project_path
pybind11::none(), // gem_path pybind11::none(), // gem_path
pybind11::none(), // external_subdir_path pybind11::none(), // external_subdir_path
@ -448,11 +445,11 @@ namespace O3DE::ProjectManager
pybind11::none(), // restricted_path pybind11::none(), // restricted_path
pybind11::none(), // repo_uri pybind11::none(), // repo_uri
pybind11::none(), // default_engines_folder pybind11::none(), // default_engines_folder
defaultProjectsFolder, QString_To_Py_Path(engineInfo.m_defaultProjectsFolder),
defaultGemsFolder, QString_To_Py_Path(engineInfo.m_defaultGemsFolder),
defaultTemplatesFolder, QString_To_Py_Path(engineInfo.m_defaultTemplatesFolder),
pybind11::none(), // default_restricted_folder pybind11::none(), // default_restricted_folder
defaultThirdPartyFolder QString_To_Py_Path(engineInfo.m_thirdPartyPath)
); );
if (registrationResult.cast<int>() != 0) if (registrationResult.cast<int>() != 0)
@ -466,7 +463,7 @@ namespace O3DE::ProjectManager
AZ::Outcome<GemInfo> PythonBindings::GetGemInfo(const QString& path, const QString& projectPath) AZ::Outcome<GemInfo> PythonBindings::GetGemInfo(const QString& path, const QString& projectPath)
{ {
GemInfo gemInfo = GemInfoFromPath(pybind11::str(path.toStdString()), pybind11::str(projectPath.toStdString())); GemInfo gemInfo = GemInfoFromPath(QString_To_Py_String(path), QString_To_Py_Path(projectPath));
if (gemInfo.IsValid()) if (gemInfo.IsValid())
{ {
return AZ::Success(AZStd::move(gemInfo)); return AZ::Success(AZStd::move(gemInfo));
@ -503,7 +500,7 @@ namespace O3DE::ProjectManager
auto result = ExecuteWithLockErrorHandling([&] auto result = ExecuteWithLockErrorHandling([&]
{ {
pybind11::str pyProjectPath = projectPath.toStdString(); auto pyProjectPath = QString_To_Py_Path(projectPath);
for (auto path : m_manifest.attr("get_all_gems")(pyProjectPath)) for (auto path : m_manifest.attr("get_all_gems")(pyProjectPath))
{ {
gems.push_back(GemInfoFromPath(path, pyProjectPath)); gems.push_back(GemInfoFromPath(path, pyProjectPath));
@ -524,10 +521,9 @@ namespace O3DE::ProjectManager
pybind11::str enabledGemsFilename; pybind11::str enabledGemsFilename;
auto result = ExecuteWithLockErrorHandling([&] auto result = ExecuteWithLockErrorHandling([&]
{ {
const pybind11::str pyProjectPath = projectPath.toStdString();
enabledGemsFilename = m_cmake.attr("get_enabled_gem_cmake_file")( enabledGemsFilename = m_cmake.attr("get_enabled_gem_cmake_file")(
pybind11::none(), // project_name pybind11::none(), // project_name
pyProjectPath); // project_path QString_To_Py_Path(projectPath)); // project_path
}); });
if (!result.IsSuccess()) if (!result.IsSuccess())
{ {
@ -558,7 +554,7 @@ namespace O3DE::ProjectManager
bool result = ExecuteWithLock( bool result = ExecuteWithLock(
[&] [&]
{ {
pybind11::str projectPath = path.toStdString(); auto projectPath = QString_To_Py_Path(path);
auto pythonRegistrationResult = m_register.attr("register")(pybind11::none(), projectPath); auto pythonRegistrationResult = m_register.attr("register")(pybind11::none(), projectPath);
// Returns an exit code so boolify it then invert result // Returns an exit code so boolify it then invert result
@ -574,10 +570,9 @@ namespace O3DE::ProjectManager
bool result = ExecuteWithLock( bool result = ExecuteWithLock(
[&] [&]
{ {
pybind11::str projectPath = path.toStdString();
auto pythonRegistrationResult = m_register.attr("register")( auto pythonRegistrationResult = m_register.attr("register")(
pybind11::none(), // engine_path pybind11::none(), // engine_path
projectPath, // project_path QString_To_Py_Path(path), // project_path
pybind11::none(), // gem_path pybind11::none(), // gem_path
pybind11::none(), // external_subdir_path pybind11::none(), // external_subdir_path
pybind11::none(), // template_path pybind11::none(), // template_path
@ -606,14 +601,12 @@ namespace O3DE::ProjectManager
{ {
ProjectInfo createdProjectInfo; ProjectInfo createdProjectInfo;
bool result = ExecuteWithLock([&] { bool result = ExecuteWithLock([&] {
pybind11::str projectPath = projectInfo.m_path.toStdString(); auto projectPath = QString_To_Py_Path(projectInfo.m_path);
pybind11::str projectName = projectInfo.m_projectName.toStdString();
pybind11::str templatePath = projectTemplatePath.toStdString();
auto createProjectResult = m_engineTemplate.attr("create_project")( auto createProjectResult = m_engineTemplate.attr("create_project")(
projectPath, projectPath,
projectName, QString_To_Py_String(projectInfo.m_projectName),
templatePath QString_To_Py_Path(projectTemplatePath)
); );
if (createProjectResult.cast<int>() == 0) if (createProjectResult.cast<int>() == 0)
{ {
@ -633,7 +626,7 @@ namespace O3DE::ProjectManager
AZ::Outcome<ProjectInfo> PythonBindings::GetProject(const QString& path) AZ::Outcome<ProjectInfo> PythonBindings::GetProject(const QString& path)
{ {
ProjectInfo projectInfo = ProjectInfoFromPath(pybind11::str(path.toStdString())); ProjectInfo projectInfo = ProjectInfoFromPath(QString_To_Py_Path(path));
if (projectInfo.IsValid()) if (projectInfo.IsValid())
{ {
return AZ::Success(AZStd::move(projectInfo)); return AZ::Success(AZStd::move(projectInfo));
@ -745,14 +738,11 @@ namespace O3DE::ProjectManager
{ {
return ExecuteWithLockErrorHandling([&] return ExecuteWithLockErrorHandling([&]
{ {
pybind11::str pyGemPath = gemPath.toStdString();
pybind11::str pyProjectPath = projectPath.toStdString();
m_enableGemProject.attr("enable_gem_in_project")( m_enableGemProject.attr("enable_gem_in_project")(
pybind11::none(), // gem name not needed as path is provided pybind11::none(), // gem name not needed as path is provided
pyGemPath, QString_To_Py_Path(gemPath),
pybind11::none(), // project name not needed as path is provided pybind11::none(), // project name not needed as path is provided
pyProjectPath QString_To_Py_Path(projectPath)
); );
}); });
} }
@ -761,21 +751,19 @@ namespace O3DE::ProjectManager
{ {
return ExecuteWithLockErrorHandling([&] return ExecuteWithLockErrorHandling([&]
{ {
pybind11::str pyGemPath = gemPath.toStdString();
pybind11::str pyProjectPath = projectPath.toStdString();
m_disableGemProject.attr("disable_gem_in_project")( m_disableGemProject.attr("disable_gem_in_project")(
pybind11::none(), // gem name not needed as path is provided pybind11::none(), // gem name not needed as path is provided
pyGemPath, QString_To_Py_Path(gemPath),
pybind11::none(), // project name not needed as path is provided pybind11::none(), // project name not needed as path is provided
pyProjectPath QString_To_Py_Path(projectPath)
); );
}); });
} }
AZ::Outcome<void, AZStd::string> PythonBindings::UpdateProject(const ProjectInfo& projectInfo) AZ::Outcome<void, AZStd::string> PythonBindings::UpdateProject(const ProjectInfo& projectInfo)
{ {
return ExecuteWithLockErrorHandling([&] bool updateProjectSucceeded = false;
auto result = ExecuteWithLockErrorHandling([&]
{ {
std::list<std::string> newTags; std::list<std::string> newTags;
for (const auto& i : projectInfo.m_userTags) for (const auto& i : projectInfo.m_userTags)
@ -783,23 +771,36 @@ namespace O3DE::ProjectManager
newTags.push_back(i.toStdString()); newTags.push_back(i.toStdString());
} }
m_editProjectProperties.attr("edit_project_props")( auto editResult = m_editProjectProperties.attr("edit_project_props")(
pybind11::str(projectInfo.m_path.toStdString()), // proj_path QString_To_Py_Path(projectInfo.m_path),
pybind11::none(), // proj_name not used pybind11::none(), // proj_name not used
pybind11::str(projectInfo.m_origin.toStdString()), // new_origin QString_To_Py_String(projectInfo.m_projectName),
pybind11::str(projectInfo.m_displayName.toStdString()), // new_display QString_To_Py_String(projectInfo.m_origin),
pybind11::str(projectInfo.m_summary.toStdString()), // new_summary QString_To_Py_String(projectInfo.m_displayName),
pybind11::str(projectInfo.m_iconPath.toStdString()), // new_icon QString_To_Py_String(projectInfo.m_summary),
QString_To_Py_String(projectInfo.m_iconPath), // new_icon
pybind11::none(), // add_tags not used pybind11::none(), // add_tags not used
pybind11::none(), // remove_tags not used pybind11::none(), // remove_tags not used
pybind11::list(pybind11::cast(newTags))); // replace_tags pybind11::list(pybind11::cast(newTags)));
updateProjectSucceeded = (editResult.cast<int>() == 0);
}); });
if (!result.IsSuccess())
{
return result;
}
else if (!updateProjectSucceeded)
{
return AZ::Failure<AZStd::string>("Failed to update project.");
}
return AZ::Success();
} }
ProjectTemplateInfo PythonBindings::ProjectTemplateInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath) ProjectTemplateInfo PythonBindings::ProjectTemplateInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath)
{ {
ProjectTemplateInfo templateInfo; ProjectTemplateInfo templateInfo;
templateInfo.m_path = Py_To_String(pybind11::str(path)); templateInfo.m_path = Py_To_String(path);
auto data = m_manifest.attr("get_template_json_data")(pybind11::none(), path, pyProjectPath); auto data = m_manifest.attr("get_template_json_data")(pybind11::none(), path, pyProjectPath);
if (pybind11::isinstance<pybind11::dict>(data)) if (pybind11::isinstance<pybind11::dict>(data))
@ -848,10 +849,9 @@ namespace O3DE::ProjectManager
QVector<ProjectTemplateInfo> templates; QVector<ProjectTemplateInfo> templates;
bool result = ExecuteWithLock([&] { bool result = ExecuteWithLock([&] {
pybind11::str pyProjectPath = projectPath.toStdString();
for (auto path : m_manifest.attr("get_templates_for_project_creation")()) for (auto path : m_manifest.attr("get_templates_for_project_creation")())
{ {
templates.push_back(ProjectTemplateInfoFromPath(path, pyProjectPath)); templates.push_back(ProjectTemplateInfoFromPath(path, QString_To_Py_Path(projectPath)));
} }
}); });

@ -75,13 +75,15 @@ namespace O3DE::ProjectManager
bool m_pythonStarted = false; bool m_pythonStarted = false;
AZ::IO::FixedMaxPath m_enginePath; AZ::IO::FixedMaxPath m_enginePath;
pybind11::handle m_engineTemplate;
AZStd::recursive_mutex m_lock; AZStd::recursive_mutex m_lock;
pybind11::handle m_engineTemplate;
pybind11::handle m_cmake; pybind11::handle m_cmake;
pybind11::handle m_register; pybind11::handle m_register;
pybind11::handle m_manifest; pybind11::handle m_manifest;
pybind11::handle m_enableGemProject; pybind11::handle m_enableGemProject;
pybind11::handle m_disableGemProject; pybind11::handle m_disableGemProject;
pybind11::handle m_editProjectProperties; pybind11::handle m_editProjectProperties;
pybind11::handle m_pathlib;
}; };
} }

@ -13,6 +13,7 @@ set(FILES
Resources/ProjectManager.qrc Resources/ProjectManager.qrc
Resources/ProjectManager.qss Resources/ProjectManager.qss
tests/ApplicationTests.cpp tests/ApplicationTests.cpp
tests/PythonBindingsTests.cpp
tests/main.cpp tests/main.cpp
tests/UtilsTests.cpp tests/UtilsTests.cpp
) )

@ -0,0 +1,74 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/UnitTest/TestTypes.h>
#include <AzTest/Utils.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <PythonBindings.h>
#include <ProjectManager_Test_Traits_Platform.h>
#include <QDir>
namespace O3DE::ProjectManager
{
class PythonBindingsTests
: public ::UnitTest::ScopedAllocatorSetupFixture
{
public:
PythonBindingsTests()
{
const AZStd::string engineRootPath{ AZ::Test::GetEngineRootPath() };
m_pythonBindings = AZStd::make_unique<PythonBindings>(AZ::IO::PathView(engineRootPath));
}
~PythonBindingsTests()
{
m_pythonBindings.reset();
}
AZStd::unique_ptr<ProjectManager::PythonBindings> m_pythonBindings;
};
TEST_F(PythonBindingsTests, PythonBindings_Start_Python_Succeeds)
{
EXPECT_TRUE(m_pythonBindings->PythonStarted());
}
TEST_F(PythonBindingsTests, PythonBindings_Create_Project_Succeeds)
{
ASSERT_TRUE(m_pythonBindings->PythonStarted());
auto templateResults = m_pythonBindings->GetProjectTemplates();
ASSERT_TRUE(templateResults.IsSuccess());
QVector<ProjectTemplateInfo> templates = templateResults.GetValue();
ASSERT_FALSE(templates.isEmpty());
// use the first registered template
QString templatePath = templates.at(0).m_path;
AZ::Test::ScopedAutoTempDirectory tempDir;
ProjectInfo projectInfo;
projectInfo.m_path = QDir::toNativeSeparators(QString(tempDir.GetDirectory()) + "/" + "TestProject");
projectInfo.m_projectName = "TestProjectName";
auto result = m_pythonBindings->CreateProject(templatePath, projectInfo);
EXPECT_TRUE(result.IsSuccess());
ProjectInfo resultProjectInfo = result.GetValue();
EXPECT_EQ(projectInfo.m_path, resultProjectInfo.m_path);
EXPECT_EQ(projectInfo.m_projectName, resultProjectInfo.m_projectName);
}
}

@ -10,7 +10,7 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* *
*/ */
// {END_LICENSE} // {END_LICENSE}
#pragma once #pragma once
@ -22,7 +22,7 @@ namespace ${SanitizedCppName}
class ${SanitizedCppName}Requests class ${SanitizedCppName}Requests
{ {
public: public:
AZ_RTTI(${SanitizedCppName}Requests, "${Random_Uuid}"); AZ_RTTI(${SanitizedCppName}Requests, "{${Random_Uuid}}");
virtual ~${SanitizedCppName}Requests() = default; virtual ~${SanitizedCppName}Requests() = default;
// Put your public methods here // Put your public methods here
}; };

@ -350,6 +350,7 @@ def _instantiate_template(template_json_data: dict,
def create_template(source_path: pathlib.Path, def create_template(source_path: pathlib.Path,
template_path: pathlib.Path, template_path: pathlib.Path,
source_name: str = None,
source_restricted_path: pathlib.Path = None, source_restricted_path: pathlib.Path = None,
source_restricted_name: str = None, source_restricted_name: str = None,
template_restricted_path: pathlib.Path = None, template_restricted_path: pathlib.Path = None,
@ -365,6 +366,8 @@ def create_template(source_path: pathlib.Path,
:param source_path: The path to the source that you want to make into a template :param source_path: The path to the source that you want to make into a template
:param template_path: the path of the template to create, can be absolute or relative to default templates path :param template_path: the path of the template to create, can be absolute or relative to default templates path
:param source_name: Name to replace within template folder with ${Name} placeholder
If not specified, the basename of the source_path parameter is used as the source_name instead
:param source_restricted_path: path to the source restricted folder :param source_restricted_path: path to the source restricted folder
:param source_restricted_name: name of the source restricted folder :param source_restricted_name: name of the source restricted folder
:param template_restricted_path: path to the templates restricted folder :param template_restricted_path: path to the templates restricted folder
@ -395,7 +398,8 @@ def create_template(source_path: pathlib.Path,
return 1 return 1
# source_name is now the last component of the source_path # source_name is now the last component of the source_path
source_name = os.path.basename(source_path) if not source_name:
source_name = os.path.basename(source_path)
sanitized_source_name = utils.sanitize_identifier_for_cpp(source_name) sanitized_source_name = utils.sanitize_identifier_for_cpp(source_name)
# if no template path, error # if no template path, error
@ -410,6 +414,16 @@ def create_template(source_path: pathlib.Path,
logger.error(f'Template path {template_path} already exists.') logger.error(f'Template path {template_path} already exists.')
return 1 return 1
# Make sure the output directory for the template is outside the source path directory
try:
template_path.relative_to(source_path)
except ValueError:
pass
else:
logger.error(f'Template output path {template_path} cannot be a subdirectory of the source_path {source_path}:\n'
f'{err}')
return 1
# template name is now the last component of the template_path # template name is now the last component of the template_path
template_name = os.path.basename(template_path) template_name = os.path.basename(template_path)
@ -938,7 +952,7 @@ def create_template(source_path: pathlib.Path,
s.write(json.dumps(json_data, indent=4) + '\n') s.write(json.dumps(json_data, indent=4) + '\n')
# copy the default preview.png # copy the default preview.png
preview_png_src = this_script_parent / 'resources' /' preview.png' preview_png_src = this_script_parent / 'resources' / 'preview.png'
preview_png_dst = template_path / 'Template' / 'preview.png' preview_png_dst = template_path / 'Template' / 'preview.png'
if not os.path.isfile(preview_png_dst): if not os.path.isfile(preview_png_dst):
shutil.copy(preview_png_src, preview_png_dst) shutil.copy(preview_png_src, preview_png_dst)
@ -987,6 +1001,7 @@ def create_template(source_path: pathlib.Path,
def create_from_template(destination_path: pathlib.Path, def create_from_template(destination_path: pathlib.Path,
template_path: pathlib.Path = None, template_path: pathlib.Path = None,
template_name: str = None, template_name: str = None,
destination_name: str = None,
destination_restricted_path: pathlib.Path = None, destination_restricted_path: pathlib.Path = None,
destination_restricted_name: str = None, destination_restricted_name: str = None,
template_restricted_path: pathlib.Path = None, template_restricted_path: pathlib.Path = None,
@ -1004,6 +1019,10 @@ def create_from_template(destination_path: pathlib.Path,
:param destination_path: the folder you want to instantiate the template into :param destination_path: the folder you want to instantiate the template into
:param template_path: the path to the template you want to instance :param template_path: the path to the template you want to instance
:param template_name: the name of the template you want to instance, resolves template_path :param template_name: the name of the template you want to instance, resolves template_path
:param destination_name: the name that will be substituted when instantiating the template.
The placeholders of ${Name} and ${SanitizedCppName} will be replaced with destination name and a sanitized
version of the destination name that is suitable as a C++ identifier. If not specified, defaults to the
last path component of the destination_path
:param destination_restricted_path: path to the projects restricted folder :param destination_restricted_path: path to the projects restricted folder
:param destination_restricted_name: name of the projects restricted folder, resolves destination_restricted_path :param destination_restricted_name: name of the projects restricted folder, resolves destination_restricted_path
:param template_restricted_path: path of the templates restricted folder :param template_restricted_path: path of the templates restricted folder
@ -1043,13 +1062,14 @@ def create_from_template(destination_path: pathlib.Path,
if template_name: if template_name:
template_path = manifest.get_registered(template_name=template_name) template_path = manifest.get_registered(template_name=template_name)
if not template_path:
logger.error(f'Could not find the template path using name {template_name}.\n'
f'Has the engine been registered yet. It can be registered via the "o3de.py register --this-engine" command')
return 1
if not os.path.isdir(template_path): if not os.path.isdir(template_path):
logger.error(f'Could not find the template {template_name}=>{template_path}') logger.error(f'Could not find the template {template_name}=>{template_path}')
return 1 return 1
# template folder name is now the last component of the template_path
template_folder_name = os.path.basename(template_path)
# the template.json should be in the template_path, make sure it's there a nd valid # the template.json should be in the template_path, make sure it's there a nd valid
template_json = template_path / 'template.json' template_json = template_path / 'template.json'
if not validation.valid_o3de_template_json(template_json): if not validation.valid_o3de_template_json(template_json):
@ -1130,7 +1150,7 @@ def create_from_template(destination_path: pathlib.Path,
return 1 return 1
# check and make sure the restricted exists # check and make sure the restricted exists
if not os.path.isdir(template_restricted_path): if template_restricted_path and not os.path.isdir(template_restricted_path):
logger.error(f'Template restricted path {template_restricted_path} does not exist.') logger.error(f'Template restricted path {template_restricted_path} does not exist.')
return 1 return 1
@ -1183,8 +1203,9 @@ def create_from_template(destination_path: pathlib.Path,
else: else:
os.makedirs(destination_path, exist_ok=force) os.makedirs(destination_path, exist_ok=force)
# destination name is now the last component of the destination_path if not destination_name:
destination_name = os.path.basename(destination_path) # destination name is now the last component of the destination_path
destination_name = os.path.basename(destination_path)
# destination name cannot be the same as a restricted platform name # destination name cannot be the same as a restricted platform name
if destination_name in restricted_platforms: if destination_name in restricted_platforms:
@ -1340,9 +1361,6 @@ def create_project(project_path: pathlib.Path,
logger.error(f'Could not find the template {template_name}=>{template_path}') logger.error(f'Could not find the template {template_name}=>{template_path}')
return 1 return 1
# template folder name is now the last component of the template_path
template_folder_name = os.path.basename(template_path)
# the template.json should be in the template_path, make sure it's there and valid # the template.json should be in the template_path, make sure it's there and valid
template_json = template_path / 'template.json' template_json = template_path / 'template.json'
if not validation.valid_o3de_template_json(template_json): if not validation.valid_o3de_template_json(template_json):
@ -1423,7 +1441,7 @@ def create_project(project_path: pathlib.Path,
return 1 return 1
# check and make sure the restricted exists # check and make sure the restricted exists
if not os.path.isdir(template_restricted_path): if template_restricted_path and not os.path.isdir(template_restricted_path):
logger.error(f'Template restricted path {template_restricted_path} does not exist.') logger.error(f'Template restricted path {template_restricted_path} does not exist.')
return 1 return 1
@ -1574,6 +1592,7 @@ def create_project(project_path: pathlib.Path,
return 1 return 1
# We created the project, now do anything extra that a project requires # We created the project, now do anything extra that a project requires
project_json = project_path / 'project.json'
# If we are not keeping the restricted in the project read the name of the restricted folder from the # If we are not keeping the restricted in the project read the name of the restricted folder from the
# restricted json and set that as this projects restricted # restricted json and set that as this projects restricted
@ -1607,7 +1626,6 @@ def create_project(project_path: pathlib.Path,
return 1 return 1
# set the "restricted_name": "restricted_name" element of the project.json # set the "restricted_name": "restricted_name" element of the project.json
project_json = project_path / 'project.json'
if not validation.valid_o3de_project_json(project_json): if not validation.valid_o3de_project_json(project_json):
logger.error(f'Project json {project_json} is not valid.') logger.error(f'Project json {project_json} is not valid.')
return 1 return 1
@ -1678,6 +1696,7 @@ def create_project(project_path: pathlib.Path,
def create_gem(gem_path: pathlib.Path, def create_gem(gem_path: pathlib.Path,
template_path: pathlib.Path = None, template_path: pathlib.Path = None,
template_name: str = None, template_name: str = None,
gem_name: str = None,
gem_restricted_path: pathlib.Path = None, gem_restricted_path: pathlib.Path = None,
gem_restricted_name: str = None, gem_restricted_name: str = None,
template_restricted_path: pathlib.Path = None, template_restricted_path: pathlib.Path = None,
@ -1697,6 +1716,10 @@ def create_gem(gem_path: pathlib.Path,
:param gem_path: the gem path, can be absolute or relative to default gems path :param gem_path: the gem path, can be absolute or relative to default gems path
:param template_path: the template path you want to instance, can be absolute or relative to default templates path :param template_path: the template path you want to instance, can be absolute or relative to default templates path
:param template_name: the name of the registered template you want to instance, defaults to DefaultGem, resolves template_path :param template_name: the name of the registered template you want to instance, defaults to DefaultGem, resolves template_path
:param gem_name: the name that will be substituted when instantiating the template.
The placeholders of ${Name} and ${SanitizedCppName} will be replaced with gem name and a sanitized
version of the gem name that is suitable as a C++ identifier. If not specified, defaults to the
last path component of the gem_path
:param gem_restricted_path: path to the gems restricted folder, can be absolute or relative to the restricted='gems' :param gem_restricted_path: path to the gems restricted folder, can be absolute or relative to the restricted='gems'
:param gem_restricted_name: str = name of the registered gems restricted path, resolves gem_restricted_path :param gem_restricted_name: str = name of the registered gems restricted path, resolves gem_restricted_path
:param template_restricted_path: the templates restricted path, can be absolute or relative to the restricted='templates' :param template_restricted_path: the templates restricted path, can be absolute or relative to the restricted='templates'
@ -1741,9 +1764,6 @@ def create_gem(gem_path: pathlib.Path,
logger.error(f'Could not find the template {template_name}=>{template_path}') logger.error(f'Could not find the template {template_name}=>{template_path}')
return 1 return 1
# template name is now the last component of the template_path
template_folder_name = os.path.basename(template_path)
# the template.json should be in the template_path, make sure it's there and valid # the template.json should be in the template_path, make sure it's there and valid
template_json = template_path / 'template.json' template_json = template_path / 'template.json'
if not validation.valid_o3de_template_json(template_json): if not validation.valid_o3de_template_json(template_json):
@ -1822,7 +1842,7 @@ def create_gem(gem_path: pathlib.Path,
f' and {template_json_restricted_path} will be used.') f' and {template_json_restricted_path} will be used.')
return 1 return 1
# check and make sure the restricted path exists # check and make sure the restricted path exists
if not os.path.isdir(template_restricted_path): if template_restricted_path and not os.path.isdir(template_restricted_path):
logger.error(f'Template restricted path {template_restricted_path} does not exist.') logger.error(f'Template restricted path {template_restricted_path} does not exist.')
return 1 return 1
@ -1880,10 +1900,9 @@ def create_gem(gem_path: pathlib.Path,
else: else:
os.makedirs(gem_path, exist_ok=force) os.makedirs(gem_path, exist_ok=force)
# gem nam # Default to the gem path basename component if gem_name has not been supplied
# if not gem_name:
# e is now the last component of the gem_path gem_name = os.path.basename(gem_path)
gem_name = os.path.basename(gem_path)
if not utils.validate_identifier(gem_name): if not utils.validate_identifier(gem_name):
logger.error(f'Gem name must be fewer than 64 characters, contain only alphanumeric, "_" or "-" characters, and start with a letter. {gem_name}') logger.error(f'Gem name must be fewer than 64 characters, contain only alphanumeric, "_" or "-" characters, and start with a letter. {gem_name}')
@ -2057,6 +2076,7 @@ def create_gem(gem_path: pathlib.Path,
def _run_create_template(args: argparse) -> int: def _run_create_template(args: argparse) -> int:
return create_template(args.source_path, return create_template(args.source_path,
args.template_path, args.template_path,
args.source_name,
args.source_restricted_path, args.source_restricted_path,
args.source_restricted_name, args.source_restricted_name,
args.template_restricted_path, args.template_restricted_path,
@ -2073,6 +2093,7 @@ def _run_create_from_template(args: argparse) -> int:
return create_from_template(args.destination_path, return create_from_template(args.destination_path,
args.template_path, args.template_path,
args.template_name, args.template_name,
args.destination_path,
args.destination_restricted_path, args.destination_restricted_path,
args.destination_restricted_name, args.destination_restricted_name,
args.template_restricted_path, args.template_restricted_path,
@ -2109,6 +2130,7 @@ def _run_create_gem(args: argparse) -> int:
return create_gem(args.gem_path, return create_gem(args.gem_path,
args.template_path, args.template_path,
args.template_name, args.template_name,
args.gem_name,
args.gem_restricted_path, args.gem_restricted_path,
args.gem_restricted_name, args.gem_restricted_name,
args.template_restricted_path, args.template_restricted_path,
@ -2159,6 +2181,16 @@ def add_args(subparsers) -> None:
help='The name of the templates restricted folder. If supplied this will resolve' help='The name of the templates restricted folder. If supplied this will resolve'
' the --template-restricted-path.') ' the --template-restricted-path.')
create_template_subparser.add_argument('-sn', '--source-name',
type=str,
help='Substitutes any file and path entries which match the source'
' name within the source-path directory with the ${Name} and'
' ${SanitizedCppName}.'
'Ex: Path substitution'
'--source-name Foo'
'<source-path>/Code/Include/FooBus.h -> <source-path>/Code/Include/${Name}Bus.h'
'Ex: File content substitution.'
'class FooRequests -> class ${SanitizedCppName}Requests')
create_template_subparser.add_argument('-srprp', '--source-restricted-platform-relative-path', type=pathlib.Path, create_template_subparser.add_argument('-srprp', '--source-restricted-platform-relative-path', type=pathlib.Path,
required=False, required=False,
default=None, default=None,
@ -2216,6 +2248,13 @@ def add_args(subparsers) -> None:
help='The name to the registered template you want to instantiate. If supplied this will' help='The name to the registered template you want to instantiate. If supplied this will'
' resolve the --template-path.') ' resolve the --template-path.')
create_from_template_subparser.add_argument('-dn', '--destination-name', type=str,
help='The name to use when substituting the ${Name} placeholder in instantiated template,'
' must be alphanumeric, '
' and can contain _ and - characters.'
' If no name is provided, will use last component of destination path.'
' Ex. New_Gem')
group = create_from_template_subparser.add_mutually_exclusive_group(required=False) group = create_from_template_subparser.add_mutually_exclusive_group(required=False)
group.add_argument('-drp', '--destination-restricted-path', type=pathlib.Path, required=False, group.add_argument('-drp', '--destination-restricted-path', type=pathlib.Path, required=False,
default=None, default=None,
@ -2372,6 +2411,12 @@ def add_args(subparsers) -> None:
create_gem_subparser = subparsers.add_parser('create-gem') create_gem_subparser = subparsers.add_parser('create-gem')
create_gem_subparser.add_argument('-gp', '--gem-path', type=pathlib.Path, required=True, create_gem_subparser.add_argument('-gp', '--gem-path', type=pathlib.Path, required=True,
help='The gem path, can be absolute or relative to default gems path') help='The gem path, can be absolute or relative to default gems path')
create_gem_subparser.add_argument('-gn', '--gem-name', type=str,
help='The name to use when substituting the ${Name} placeholder for the gem,'
' must be alphanumeric, '
' and can contain _ and - characters.'
' If no name is provided, will use last component of gem path.'
' Ex. New_Gem')
group = create_gem_subparser.add_mutually_exclusive_group(required=False) group = create_gem_subparser.add_mutually_exclusive_group(required=False)
group.add_argument('-tp', '--template-path', type=pathlib.Path, required=False, group.add_argument('-tp', '--template-path', type=pathlib.Path, required=False,

@ -16,7 +16,7 @@ import pathlib
import sys import sys
import logging import logging
from o3de import manifest from o3de import manifest, utils
logger = logging.getLogger() logger = logging.getLogger()
logging.basicConfig() logging.basicConfig()
@ -29,8 +29,16 @@ def get_project_props(name: str = None, path: pathlib.Path = None) -> dict:
return None return None
return proj_json return proj_json
def edit_project_props(proj_path, proj_name, new_origin, new_display, def edit_project_props(proj_path: pathlib.Path,
new_summary, new_icon, new_tags, delete_tags, replace_tags) -> int: proj_name: str = None,
new_name: str = None,
new_origin: str = None,
new_display: str = None,
new_summary: str = None,
new_icon: str = None,
new_tags: str or list = None,
delete_tags: str or list = None,
replace_tags: str or list = None) -> int:
proj_json = get_project_props(proj_name, proj_path) proj_json = get_project_props(proj_name, proj_path)
if not proj_json: if not proj_json:
@ -38,6 +46,11 @@ def edit_project_props(proj_path, proj_name, new_origin, new_display,
if new_origin: if new_origin:
proj_json['origin'] = new_origin proj_json['origin'] = new_origin
if new_name:
if not utils.validate_identifier(new_name):
logger.error(f'Project name must be fewer than 64 characters, contain only alphanumeric, "_" or "-" characters, and start with a letter. {new_name}')
return 1
proj_json['project_name'] = new_name
if new_display: if new_display:
proj_json['display_name'] = new_display proj_json['display_name'] = new_display
if new_summary: if new_summary:
@ -54,9 +67,9 @@ def edit_project_props(proj_path, proj_name, new_origin, new_display,
if tag in proj_json['user_tags']: if tag in proj_json['user_tags']:
proj_json['user_tags'].remove(tag) proj_json['user_tags'].remove(tag)
else: else:
logger.warn(f'{tag} not found in user_tags for removal.') logger.warning(f'{tag} not found in user_tags for removal.')
else: else:
logger.warn(f'user_tags property not found for removal of {remove_tags}.') logger.warning('user_tags property not found.')
if replace_tags: if replace_tags:
tag_list = [replace_tags] if isinstance(replace_tags, str) else replace_tags tag_list = [replace_tags] if isinstance(replace_tags, str) else replace_tags
proj_json['user_tags'] = tag_list proj_json['user_tags'] = tag_list
@ -67,13 +80,14 @@ def edit_project_props(proj_path, proj_name, new_origin, new_display,
def _edit_project_props(args: argparse) -> int: def _edit_project_props(args: argparse) -> int:
return edit_project_props(args.project_path, return edit_project_props(args.project_path,
args.project_name, args.project_name,
args.project_origin, args.project_new_name,
args.project_display, args.project_origin,
args.project_summary, args.project_display,
args.project_icon, args.project_summary,
args.add_tags, args.project_icon,
args.delete_tags, args.add_tags,
args.replace_tags) args.delete_tags,
args.replace_tags)
def add_parser_args(parser): def add_parser_args(parser):
group = parser.add_mutually_exclusive_group(required=True) group = parser.add_mutually_exclusive_group(required=True)
@ -82,6 +96,8 @@ def add_parser_args(parser):
group.add_argument('-pn', '--project-name', type=str, required=False, group.add_argument('-pn', '--project-name', type=str, required=False,
help='The name of the project.') help='The name of the project.')
group = parser.add_argument_group('properties', 'arguments for modifying individual project properties.') group = parser.add_argument_group('properties', 'arguments for modifying individual project properties.')
group.add_argument('-pnn', '--project-new-name', type=str, required=False,
help='Sets the name for the project.')
group.add_argument('-po', '--project-origin', type=str, required=False, group.add_argument('-po', '--project-origin', type=str, required=False,
help='Sets description or url for project origin (such as project host, repository, owner...etc).') help='Sets description or url for project origin (such as project host, repository, owner...etc).')
group.add_argument('-pd', '--project-display', type=str, required=False, group.add_argument('-pd', '--project-display', type=str, required=False,

@ -41,3 +41,10 @@ ly_add_pytest(
TEST_SUITE smoke TEST_SUITE smoke
EXCLUDE_TEST_RUN_TARGET_FROM_IDE EXCLUDE_TEST_RUN_TARGET_FROM_IDE
) )
ly_add_pytest(
NAME o3de_template
PATH ${CMAKE_CURRENT_LIST_DIR}/unit_test_engine_template.py
TEST_SUITE smoke
EXCLUDE_TEST_RUN_TARGET_FROM_IDE
)

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save