diff --git a/README.md b/README.md index ce5c304..8089b4a 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,10 @@ [![Build status](https://travis-ci.org/roedesh/nxstart.svg?branch=master)](https://travis-ci.org/roedesh/nxstart) ## Features -- Generate a C/C++ ([libnx](https://github.com/switchbrew/libnx)) project using `nxstart cpp` -- Generate a Javascript ([BrewJS](https://github.com/BrewJS)) project using `nxstart js` +- Generate a [libnx](https://github.com/switchbrew/libnx) (C++) project using `nxstart libnx` +- Generate a [libtransistor](https://github.com/reswitched/libtransistor) (C) project using `nxstart libt` +- Generate a [BrewJS](https://github.com/BrewJS) (Javascript) project using `nxstart brewjs` +- Generate a [PyNX](https://github.com/nx-python/PyNX) (Python) project using `nxstart pynx` ## Installation @@ -31,17 +33,19 @@ python setup.py install ``` You may need to run the above commands with ``sudo``. -## Creating a C/C++ (libnx) project -Run `nxstart cpp`. It will ask for a project name, author name and if you are +## Creating a libnx (C++) project +Run `nxstart libnx`. It will ask for a project name, author name and if you are using CLion (IDE by Jetbrains). If you say yes to CLion, `CMakeLists.txt` will be included. The following project structure will be created: ``` project +│ .editorconfig +│ .gitignore │ CMakeLists.txt // Only if you use CLion -│ Makefile │ icon.jpg +│ Makefile │ README.md │ └───data @@ -52,19 +56,51 @@ project │ main.cpp // Your main application file ``` -## Creating a Javascript (BrewJS) project -Run `nxstart js`. It will ask for a project name and author name. The following project structure will be created: +## Creating a libtransistor (C) project +Run `nxstart libt`. It will ask for a project name, author name and if you are +using CLion (IDE by Jetbrains). If you say yes to CLion, `CMakeLists.txt` will be included. + +The following project structure will be created: ``` project -│ .editorconfig +│ .editorconfig +│ .gitignore +│ CMakeLists.txt // Only if you use CLion +│ icon.jpg +│ main.c // Your main application file +│ Makefile +│ README.md +│ +``` + +## Creating a BrewJS (Javascript) project +Run `nxstart brewjs`. It will ask for a project name and author name. The following project structure will be created: + +``` +project +│ .editorconfig +│ .gitignore │ HOW-TO-RUN.txt // Explains how to run a BrewJS app on the Switch │ index.js // Your main application file +│ README.md │ └───assets │ ``` +## Creating a PyNX (Python) project +Run `nxstart pynx`. It will ask for a project name and author name. The following project structure will be created: + +``` +project +│ .editorconfig +│ .gitignore +│ main.py // Your main application file +│ README.md +│ +``` + ## Skip prompts To skip the prompts, provide the necessary flags. For example: ```bash @@ -76,5 +112,3 @@ Or if you don't use CLion: nxstart -n "My new project" -a "John Doe" cpp --no-clion ``` -Support for -[PyNX](https://github.com/nx-python/PyNX) projects will be added soon. diff --git a/README.rst b/README.rst index 9441237..d453400 100644 --- a/README.rst +++ b/README.rst @@ -3,6 +3,19 @@ nx-start Project generator for Nintendo Switch homebrews. A work in progress. +.. image:: https://travis-ci.org/roedesh/nxstart.svg?branch=master + :target: https://travis-ci.org/roedesh/nxstart + + + +Features +======== +- Generate a libnx (C++) project using ``nxstart libnx`` +- Generate a libtransistor (C) project using ``nxstart libt`` +- Generate a BrewJS (Javascript) project using ``nxstart brewjs`` +- Generate a PyNX (Python) project using ``nxstart pynx`` + + Installation ============ @@ -29,41 +42,78 @@ Or, you can `download the source code `_ for You may need to run the above commands with ``sudo``. -Creating a C++ (libnx) project -=============== -Run ``nxstart cpp``. It will ask for a project name, author name and if you are +Creating a libnx (C++) project +============================== +Run ``nxstart libnx``. It will ask for a project name, author name and if you are using CLion (IDE by Jetbrains). If you say yes to CLion, ``CMakeLists.txt`` will be included. The following project structure will be created: .. code-block:: bash - project - │ CMakeLists.txt // Only if you use CLion - │ Makefile - │ icon.jpg - │ README.md - │ - └───data - │ - └───include - │ - └───source - │ main.cpp // Your main application file - -Creating a JS (BrewJS) project -=============== -Run ``nxstart js``. It will ask for a project name, author name. The following project structure will be created: + project + │ .editorconfig + │ .gitignore + │ CMakeLists.txt // Only if you use CLion + │ Makefile + │ icon.jpg + │ README.md + │ + └───data + │ + └───include + │ + └───source + │ main.cpp // Your main application file + + +Creating a libtransistor (C) project +==================================== +Run ``nxstart libt``. It will ask for a project name, author name and if you are +using CLion (IDE by Jetbrains). If you say yes to CLion, ``CMakeLists.txt`` will be included. + +The following project structure will be created: .. code-block:: bash - project - │ .editorconfig - │ HOW-TO-RUN.txt // Explains how to run a BrewJS app on the Switch. - │ index.js // Your main application file - │ - └───assets - │ + project + │ .editorconfig + │ .gitignore + │ CMakeLists.txt // Only if you use CLion + │ main.c + │ Makefile + │ icon.jpg + │ README.md + │ + +Creating a BrewJS (Javascript) project +====================================== +Run ``nxstart brewjs``. It will ask for a project name, author name. The following project structure will be created: + +.. code-block:: bash + + project + │ .editorconfig + │ .gitignore + │ HOW-TO-RUN.txt // Explains how to run a BrewJS app on the Switch. + │ index.js // Your main application file + │ README.md + │ + └───assets + │ + +Creating a PyNX (Python) project +================================ +Run ``nxstart pynx``. It will ask for a project name, author name. The following project structure will be created: + +.. code-block:: bash + + project + │ .editorconfig + │ .gitignore + │ main.py // Your main application file + │ README.md + │ Skip prompts =============== @@ -78,7 +128,3 @@ Or if you don't use CLion: .. code-block:: bash $ nxstart -n "My new project" -a "John Doe" cpp --no-clion - - -Support for -`PyNX `_ projects will be added soon. \ No newline at end of file diff --git a/nxstart/app.py b/nxstart/app.py index 7f3e0c7..3608026 100644 --- a/nxstart/app.py +++ b/nxstart/app.py @@ -25,13 +25,36 @@ def libnx(name, author, clion, cwd): filebuilder.generic.create_readme_file(folder_path, name) if clion: - filebuilder.libnx.modify_cmake_lists_file(folder_path, folder_name) + filebuilder.generic.modify_cmake_lists_file(folder_path, folder_name) else: - filebuilder.libnx.remove_cmake_lists_file(folder_path) + filebuilder.generic.remove_cmake_lists_file(folder_path) click.echo("Successfully created the libnx project!") +def libt(name, author, clion, cwd): + """ + Function that holds the logic for the 'libt' command. + + :param name: Name of the project + :param author: Name of the author + :param clion: Using CLion + :param cwd: Current working directory + """ + folder_name, folder_path = generate_folder_name_and_path(name, cwd) + check_and_create_directory(folder_path) + + filebuilder.libt.create_libt_project(folder_path, name, author) + filebuilder.generic.create_readme_file(folder_path, name) + + if clion: + filebuilder.generic.modify_cmake_lists_file(folder_path, folder_name) + else: + filebuilder.generic.remove_cmake_lists_file(folder_path) + + click.echo("Successfully created the libtransistor project!") + + def brewjs(name, author, cwd): """ Function that holds the logic for the 'brewjs' command. diff --git a/nxstart/cli.py b/nxstart/cli.py index 865e421..2d3e058 100644 --- a/nxstart/cli.py +++ b/nxstart/cli.py @@ -5,6 +5,10 @@ import click import os +import py +import pytest +import sys + from nxstart import app @@ -28,18 +32,24 @@ def __init__(self): help='The full name of the author') @pass_context def cli(ctx, name, author): + ctx.name = name + ctx.author = author + + +@cli.command('libnx', short_help='create a new libnx project (C++)') +@click.option('--clion/--no-clion', default=False, prompt='Are you using CLion?', help='include CMakeLists.txt') +@pass_context +def libnx(ctx, clion): """ - Main command group. + Command for generating a libnx project. :param ctx: Context - :param name: Project name - :param author: Project author + :param clion: Using CLion """ - ctx.name = name - ctx.author = author + app.libnx(ctx.name, ctx.author, clion, ctx.cwd) -@cli.command('libnx', short_help='generate a new libnx project') +@cli.command('libt', short_help='create a new libtransistor project (C)') @click.option('--clion/--no-clion', default=False, prompt='Are you using CLion?', help='include CMakeLists.txt') @pass_context def libnx(ctx, clion): @@ -52,7 +62,7 @@ def libnx(ctx, clion): app.libnx(ctx.name, ctx.author, clion, ctx.cwd) -@cli.command('brewjs', short_help='generate a new BrewJS project') +@cli.command('brewjs', short_help='create a new BrewJS project (Javascript)') @pass_context def brewjs(ctx): """ @@ -63,11 +73,11 @@ def brewjs(ctx): app.brewjs(ctx.name, ctx.author, ctx.cwd) -@cli.command('pynx', short_help='generate a new PyNX project') +@cli.command('pynx', short_help='create a new PyNX project (Python)') @pass_context def pynx(ctx): """ - Command for generating a BrewJS project. + Command for generating a PyNX project. :param ctx: Context """ diff --git a/nxstart/filebuilder/__init__.py b/nxstart/filebuilder/__init__.py index 66c1ac3..61939cd 100644 --- a/nxstart/filebuilder/__init__.py +++ b/nxstart/filebuilder/__init__.py @@ -1 +1 @@ -from nxstart.filebuilder import libnx, generic, brewjs, pynx +from nxstart.filebuilder import libnx, libt, generic, brewjs, pynx diff --git a/nxstart/filebuilder/generic.py b/nxstart/filebuilder/generic.py index bb929b2..1d329fa 100644 --- a/nxstart/filebuilder/generic.py +++ b/nxstart/filebuilder/generic.py @@ -26,4 +26,25 @@ def create_readme_file(folder_path, name): replace_in_file(new_readme_file, new_readme_file_replacements) +def remove_cmake_lists_file(folder_path): + """ + Removes the CMakeLists.txt file inside the folder at folder_path. + + :param folder_path: Path to created folder + """ + cmake_lists_file = os.path.join(folder_path, 'CMakeLists.txt') + os.remove(cmake_lists_file) + +def modify_cmake_lists_file(folder_path, folder_name): + """ + Modifies the CMakeLists.txt file from folder_path, and will use folder_name as the project name. + + :param folder_path: Path to created folder + :param folder_name: Project folder name + """ + cmake_lists_file = os.path.join(folder_path, 'CMakeLists.txt') + cmake_lists_file_replacements = { + 'FOLDER_NAME_PLACEHOLDER': folder_name + } + replace_in_file(cmake_lists_file, cmake_lists_file_replacements) diff --git a/nxstart/filebuilder/libnx.py b/nxstart/filebuilder/libnx.py index 9642903..e81ef13 100644 --- a/nxstart/filebuilder/libnx.py +++ b/nxstart/filebuilder/libnx.py @@ -35,27 +35,3 @@ def create_libnx_project(folder_path, name, author): 'APP_AUTHOR_PLACEHOLDER': author } replace_in_file(makefile, makefile_replacements) - - -def remove_cmake_lists_file(folder_path): - """ - Removes the CMakeLists.txt file inside the folder at folder_path. - - :param folder_path: Path to created folder - """ - cmake_lists_file = os.path.join(folder_path, 'CMakeLists.txt') - os.remove(cmake_lists_file) - - -def modify_cmake_lists_file(folder_path, folder_name): - """ - Modifies the CMakeLists.txt file from folder_path, and will use folder_name as the project name. - - :param folder_path: Path to created folder - :param folder_name: Project folder name - """ - cmake_lists_file = os.path.join(folder_path, 'CMakeLists.txt') - cmake_lists_file_replacements = { - 'FOLDER_NAME_PLACEHOLDER': folder_name - } - replace_in_file(cmake_lists_file, cmake_lists_file_replacements) diff --git a/nxstart/filebuilder/libt.py b/nxstart/filebuilder/libt.py new file mode 100644 index 0000000..74f6257 --- /dev/null +++ b/nxstart/filebuilder/libt.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +"""Includes functions for copying the libtransistor template files.""" + +import datetime +import os +from distutils.dir_util import copy_tree + +from nxstart.utils.files import get_full_path, replace_in_file + + +def create_libt_project(folder_path, name, author): + """ + Copies the files from templates/base to folder_path and modifies Makefile and source/main.cpp + to include the project name, author name and current date. + + :param folder_name: Created folder name + :param folder_path: Path to copy the files to + :param name: Name of the project + :param author: Name of the author + """ + template_folder = get_full_path(os.path.join('templates', 'libt')) + copy_tree(template_folder, folder_path) + + main_c_file = os.path.join(folder_path, 'main.c') + main_c_replacements = { + 'APP_AUTHOR_PLACEHOLDER': author, + 'APP_NAME_PLACEHOLDER': name, + 'DATE_PLACEHOLDER': datetime.datetime.now().strftime("%Y-%m-%d") + } + replace_in_file(main_c_file, main_c_replacements) + + makefile = os.path.join(folder_path, 'Makefile') + makefile_replacements = { + 'APP_NAME_PLACEHOLDER': name, + 'APP_AUTHOR_PLACEHOLDER': author + } + replace_in_file(makefile, makefile_replacements) \ No newline at end of file diff --git a/nxstart/templates/libnx/source/main.cpp b/nxstart/templates/libnx/source/main.cpp index 4b5b419..3672277 100755 --- a/nxstart/templates/libnx/source/main.cpp +++ b/nxstart/templates/libnx/source/main.cpp @@ -20,7 +20,7 @@ int main(int argc, char **argv) { socketInitializeDefault(); // Sets up printf to be passed to our nxlink server on the computer - nxlinkStdio(); + // nxlinkStdio(); // Setup the console consoleInit(nullptr); diff --git a/nxstart/templates/libt/.editorconfig b/nxstart/templates/libt/.editorconfig new file mode 100644 index 0000000..bae7d1c --- /dev/null +++ b/nxstart/templates/libt/.editorconfig @@ -0,0 +1,15 @@ +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +trim_trailing_whitespace = true +end_of_line = lf +insert_final_newline = true + +# Tab indentation (no size specified) +[Makefile] +indent_style = tab + +[*.{c,h,cpp,hpp}] +indent_size = 2 \ No newline at end of file diff --git a/nxstart/templates/libt/.gitignore b/nxstart/templates/libt/.gitignore new file mode 100644 index 0000000..dab799c --- /dev/null +++ b/nxstart/templates/libt/.gitignore @@ -0,0 +1,53 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + diff --git a/nxstart/templates/libt/CMakeLists.txt b/nxstart/templates/libt/CMakeLists.txt new file mode 100644 index 0000000..dda8504 --- /dev/null +++ b/nxstart/templates/libt/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.9) +project(FOLDER_NAME_PLACEHOLDER) + +set(CMAKE_CXX_STANDARD 11) + +include_directories($ENV{LIBTRANSISTOR_HOME}/lib) +include_directories($ENV{LIBTRANSISTOR_HOME}/include) +include_directories($ENV{DEVKITPRO}/libnx/lib) +include_directories($ENV{DEVKITPRO}/libnx/include) +include_directories($ENV{DEVKITARM}) +link_directories($ENV{DEVKITPRO}/libnx/lib) +link_directories($ENV{DEVKITPRO}/libnx/include) +link_directories($ENV{LIBTRANSISTOR_HOME}/bin) +link_directories($ENV{LIBTRANSISTOR_HOME}/include) +add_executable(FOLDER_NAME_PLACEHOLDER source/main.cpp) diff --git a/nxstart/templates/libt/Makefile b/nxstart/templates/libt/Makefile new file mode 100755 index 0000000..5653a8b --- /dev/null +++ b/nxstart/templates/libt/Makefile @@ -0,0 +1,24 @@ +NRO_ICON := icon.jpg +NRO_NAME := APP_NAME_PLACEHOLDER +NRO_DEVELOPER := APP_AUTHOR_PLACEHOLDER +NRO_VERSION := 0.0.1 + +TARGET := $(subst $e ,_,$(notdir $(NRO_NAME))) +OBJECTS := main.o + +all: $(TARGET).nro $(TARGET).nso + +clean: + rm -f *.o *.nro *.nso *.so + +# include libtransistor rules +ifndef LIBTRANSISTOR_HOME + $(error LIBTRANSISTOR_HOME must be set) +endif +include $(LIBTRANSISTOR_HOME)/libtransistor.mk + +$(TARGET).nro.so: $(OBJECTS) $(LIBTRANSITOR_NRO_LIB) $(LIBTRANSISTOR_COMMON_LIBS) + $(LD) $(LD_FLAGS) -o $@ $(OBJECTS) $(LIBTRANSISTOR_NRO_LDFLAGS) + +$(TARGET).nso.so: $(OBJECTS) $(LIBTRANSITOR_NSO_LIB) $(LIBTRANSISTOR_COMMON_LIBS) + $(LD) $(LD_FLAGS) -o $@ $(OBJECTS) $(LIBTRANSISTOR_NSO_LDFLAGS) diff --git a/nxstart/templates/libt/icon.jpg b/nxstart/templates/libt/icon.jpg new file mode 100755 index 0000000..ab7cbc6 Binary files /dev/null and b/nxstart/templates/libt/icon.jpg differ diff --git a/nxstart/templates/libt/main.c b/nxstart/templates/libt/main.c new file mode 100755 index 0000000..e5b6433 --- /dev/null +++ b/nxstart/templates/libt/main.c @@ -0,0 +1,18 @@ +/** + * @file main.c + * @author APP_AUTHOR_PLACEHOLDER + * @date DATE_PLACEHOLDER + * @version 0.0.1 + * + * This is the main file for APP_NAME_PLACEHOLDER. + * Original example file by libtransistor. + */ + +#include + +#include + +int main() { + printf("Hello, world!\n"); + return 0; +} diff --git a/nxstart/templates/pynx/.gitignore b/nxstart/templates/pynx/.gitignore new file mode 100644 index 0000000..7bff731 --- /dev/null +++ b/nxstart/templates/pynx/.gitignore @@ -0,0 +1,105 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + diff --git a/nxstart/tests/test_brewjs.py b/nxstart/tests/test_cli_brewjs.py similarity index 89% rename from nxstart/tests/test_brewjs.py rename to nxstart/tests/test_cli_brewjs.py index 2d70f34..7656a0c 100644 --- a/nxstart/tests/test_brewjs.py +++ b/nxstart/tests/test_cli_brewjs.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -"""Includes test for the 'js' command""" +"""Includes test for the 'brewjs' command""" from click.testing import CliRunner diff --git a/nxstart/tests/test_libnx.py b/nxstart/tests/test_cli_libnx.py similarity index 100% rename from nxstart/tests/test_libnx.py rename to nxstart/tests/test_cli_libnx.py diff --git a/nxstart/tests/test_cli_libt.py b/nxstart/tests/test_cli_libt.py new file mode 100644 index 0000000..ab38a94 --- /dev/null +++ b/nxstart/tests/test_cli_libt.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +"""Includes tests for the 'libt' command""" + +from click.testing import CliRunner + +from nxstart.cli import cli + + +def test_libt_with_clion(): + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(cli, ['-n', 'Test project', '-a', 'Ruud Schroën', 'libt', '--clion']) + assert not result.exception + assert result.output.endswith('Successfully created the libnx project!\n') + + +def test_libt_without_clion(): + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(cli, ['-n', 'Test project', '-a', 'Ruud Schroën', 'libt', '--no-clion']) + assert not result.exception + assert result.output.endswith('Successfully created the libnx project!\n') diff --git a/nxstart/tests/test_pynx.py b/nxstart/tests/test_cli_pynx.py similarity index 100% rename from nxstart/tests/test_pynx.py rename to nxstart/tests/test_cli_pynx.py diff --git a/nxstart/tests/test_utils_files.py b/nxstart/tests/test_utils_files.py new file mode 100644 index 0000000..4271a52 --- /dev/null +++ b/nxstart/tests/test_utils_files.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +"""Includes tests for the functions in nxstart.utils.files""" + +import os +import shutil + +from nxstart.utils.files import check_and_create_directory, PROJECT_ROOT, get_full_path + + +def test_get_full_path(): + path = os.path.join(PROJECT_ROOT, 'test.py') + path_two = get_full_path('test.py') + assert path == path_two + + +def test_check_and_create_directory(): + new_folder_name = 'testfolder' + new_folder_path = os.path.join(PROJECT_ROOT, 'tests', new_folder_name) + + check_and_create_directory(new_folder_path) + assert os.path.isdir(new_folder_path) + shutil.rmtree(new_folder_path) \ No newline at end of file diff --git a/nxstart/utils/files.py b/nxstart/utils/files.py index 6fd572d..c494de4 100644 --- a/nxstart/utils/files.py +++ b/nxstart/utils/files.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- + +"""Includes functions for working with the filesystem.""" + from os.path import join, dirname import os diff --git a/nxstart/utils/strings.py b/nxstart/utils/strings.py index 4c6ab95..25d7b6a 100644 --- a/nxstart/utils/strings.py +++ b/nxstart/utils/strings.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- + +"""Includes functions for manipulating strings.""" + import os diff --git a/nxstart/version.py b/nxstart/version.py index 0404d81..abeeedb 100644 --- a/nxstart/version.py +++ b/nxstart/version.py @@ -1 +1 @@ -__version__ = '0.3.0' +__version__ = '0.4.0'