diff --git a/askama_derive/src/config.rs b/askama_derive/src/config.rs index 2ac2ae6f1..2774ff997 100644 --- a/askama_derive/src/config.rs +++ b/askama_derive/src/config.rs @@ -12,7 +12,8 @@ use proc_macro2::Span; #[cfg(feature = "config")] use serde_derive::Deserialize; -use crate::{CompileError, FileInfo, OnceMap}; +use crate::paths::diff_paths; +use crate::{CompileError, FileInfo, OnceMap, fmt_left, fmt_right}; #[derive(Debug)] pub(crate) struct Config { @@ -41,6 +42,7 @@ struct ConfigKey<'a> { root: Cow<'a, Path>, source: Cow<'a, str>, config_path: Option>, + caller_dir: Option>, template_whitespace: Option, } @@ -55,6 +57,10 @@ impl ToOwned for ConfigKey<'_> { .config_path .as_ref() .map(|s| Cow::Owned(s.as_ref().to_owned())), + caller_dir: self + .caller_dir + .as_ref() + .map(|s| Cow::Owned(s.as_ref().to_owned())), template_whitespace: self.template_whitespace, }; OwnedConfigKey(Box::leak(Box::new(owned_key))) @@ -74,6 +80,7 @@ impl Config { config_path: Option<&str>, template_whitespace: Option, config_span: Option, + caller_dir: Option<&Path>, full_config_path: Option, ) -> Result<&'static Config, CompileError> { static CACHE: ManuallyDrop>> = @@ -83,6 +90,7 @@ impl Config { root: Cow::Owned(manifest_root()), source: source.into(), config_path: config_path.map(Cow::Borrowed), + caller_dir: caller_dir.map(Cow::Borrowed), template_whitespace, }, |key| { @@ -93,9 +101,18 @@ impl Config { |config| *config, ) } -} -impl Config { + pub(crate) fn rel_path<'p>(&self, path: &'p Path) -> Cow<'p, Path> { + self.caller_dir() + .and_then(|caller_dir| diff_paths(path, caller_dir)) + .map_or(Cow::Borrowed(path), Cow::Owned) + } + + #[inline] + fn caller_dir(&self) -> Option<&Path> { + self._key.0.caller_dir.as_deref() + } + fn new_uncached( key: OwnedConfigKey, config_span: Option, @@ -189,35 +206,68 @@ impl Config { start_at: Option<&Path>, file_info: Option>, ) -> Result, CompileError> { - let path = 'find_path: { - if let Some(root) = start_at { - let relative = root.with_file_name(path); - if relative.exists() { - break 'find_path relative; - } - } - for dir in &self.dirs { - let rooted = dir.join(path); - if rooted.exists() { - break 'find_path rooted; - } - } - return Err(CompileError::new( - format_args!( + let path = Path::new(path); + let err = match find_template_sub(path, &self.dirs, start_at, self.caller_dir()) + .map(|p| p.canonicalize()) + { + Some(Ok(path)) => return Ok(path.into()), + Some(Err(err)) => Some(err), + None => None, + }; + + Err(CompileError::new( + match err { + Some(err) => fmt_left!( + move "could not canonicalize path {:?}: {err}", + path.display(), + ), + None => fmt_right!( "template {:?} not found in directories {:?}", - path, self.dirs, + path.display(), + self.dirs, ), - file_info, - )); - }; - match path.canonicalize() { - Ok(path) => Ok(path.into()), - Err(err) => Err(CompileError::new( - format_args!("could not canonicalize path {path:?}: {err}"), - file_info, - )), + }, + file_info, + )) + } +} + +fn find_template_sub<'a>( + path: &'a Path, + dirs: &[PathBuf], + start_at: Option<&Path>, + caller_dir: Option<&Path>, +) -> Option> { + // If the path is absolute, then there is no need to look into different directories. + if path.is_absolute() { + return path.exists().then_some(Cow::Borrowed(path)); + } + + // First look into the same directory as the including file. + if let Some(root) = start_at { + let relative = root.with_file_name(path); + if relative.exists() { + return Some(Cow::Owned(relative)); + } + } + + // Then look into the into the configured directories, `["$WORKSPACE/templates"]` by default. + for dir in dirs { + let rooted = dir.join(path); + if rooted.exists() { + return Some(Cow::Owned(rooted)); } } + + // Lastly, look into the folder where the struct was declared. + if let Some(caller_dir) = caller_dir { + let rooted = caller_dir.join(path); + if rooted.exists() { + return Some(Cow::Owned(rooted)); + } + } + + None } #[derive(Debug, Default)] @@ -396,7 +446,7 @@ mod tests { fn test_default_config() { let mut root = manifest_root(); root.push("templates"); - let config = Config::new("", None, None, None, None).unwrap(); + let config = Config::new("", None, None, None, None, None).unwrap(); assert_eq!(config.dirs, vec![root]); } @@ -405,7 +455,8 @@ mod tests { fn test_config_dirs() { let mut root = manifest_root(); root.push("tpl"); - let config = Config::new("[general]\ndirs = [\"tpl\"]", None, None, None, None).unwrap(); + let config = + Config::new("[general]\ndirs = [\"tpl\"]", None, None, None, None, None).unwrap(); assert_eq!(config.dirs, vec![root]); } @@ -419,7 +470,7 @@ mod tests { #[test] fn find_absolute() { - let config = Config::new("", None, None, None, None).unwrap(); + let config = Config::new("", None, None, None, None, None).unwrap(); let root = config.find_template("a.html", None, None).unwrap(); let path = config .find_template("sub/b.html", Some(&root), None) @@ -430,14 +481,14 @@ mod tests { #[test] #[should_panic] fn find_relative_nonexistent() { - let config = Config::new("", None, None, None, None).unwrap(); + let config = Config::new("", None, None, None, None, None).unwrap(); let root = config.find_template("a.html", None, None).unwrap(); config.find_template("c.html", Some(&root), None).unwrap(); } #[test] fn find_relative() { - let config = Config::new("", None, None, None, None).unwrap(); + let config = Config::new("", None, None, None, None, None).unwrap(); let root = config.find_template("sub/b.html", None, None).unwrap(); let path = config.find_template("c.html", Some(&root), None).unwrap(); assert_eq_rooted(&path, "sub/c.html"); @@ -445,7 +496,7 @@ mod tests { #[test] fn find_relative_sub() { - let config = Config::new("", None, None, None, None).unwrap(); + let config = Config::new("", None, None, None, None, None).unwrap(); let root = config.find_template("sub/b.html", None, None).unwrap(); let path = config .find_template("sub1/d.html", Some(&root), None) @@ -470,7 +521,7 @@ mod tests { "#; let default_syntax = Syntax::default(); - let config = Config::new(raw_config, None, None, None, None).unwrap(); + let config = Config::new(raw_config, None, None, None, None, None).unwrap(); assert_eq!(config.default_syntax, "foo"); let foo = config.syntaxes.get("foo").unwrap(); @@ -502,7 +553,7 @@ mod tests { "#; let default_syntax = Syntax::default(); - let config = Config::new(raw_config, None, None, None, None).unwrap(); + let config = Config::new(raw_config, None, None, None, None, None).unwrap(); assert_eq!(config.default_syntax, "foo"); let foo = config.syntaxes.get("foo").unwrap(); @@ -539,7 +590,7 @@ mod tests { default_syntax = "emoji" "#; - let config = Config::new(raw_config, None, None, None, None).unwrap(); + let config = Config::new(raw_config, None, None, None, None, None).unwrap(); assert_eq!(config.default_syntax, "emoji"); let foo = config.syntaxes.get("emoji").unwrap(); @@ -567,7 +618,7 @@ mod tests { name = "too_short" block_start = "<" "#; - let config = Config::new(raw_config, None, None, None, None); + let config = Config::new(raw_config, None, None, None, None, None); assert_eq!( expect_err(config).msg, r#"delimiters must be at least two characters long. The opening block delimiter ("<") is too short"#, @@ -578,7 +629,7 @@ mod tests { name = "contains_ws" block_start = " {{ " "#; - let config = Config::new(raw_config, None, None, None, None); + let config = Config::new(raw_config, None, None, None, None, None); assert_eq!( expect_err(config).msg, r#"delimiters may not contain white spaces. The opening block delimiter (" {{ ") contains white spaces"#, @@ -591,7 +642,7 @@ mod tests { expr_start = "{{$" comment_start = "{{#" "#; - let config = Config::new(raw_config, None, None, None, None); + let config = Config::new(raw_config, None, None, None, None, None); assert_eq!( expect_err(config).msg, r#"an opening delimiter may not be the prefix of another delimiter. The block delimiter ("{{") clashes with the expression delimiter ("{{$")"#, @@ -606,7 +657,7 @@ mod tests { syntax = [{ name = "default" }] "#; - let _config = Config::new(raw_config, None, None, None, None).unwrap(); + let _config = Config::new(raw_config, None, None, None, None, None).unwrap(); } #[cfg(feature = "config")] @@ -618,7 +669,7 @@ mod tests { { name = "foo", block_start = "%%" } ] "#; - let _config = Config::new(raw_config, None, None, None, None).unwrap(); + let _config = Config::new(raw_config, None, None, None, None, None).unwrap(); } #[cfg(feature = "config")] @@ -630,7 +681,7 @@ mod tests { default_syntax = "foo" "#; - let _config = Config::new(raw_config, None, None, None, None).unwrap(); + let _config = Config::new(raw_config, None, None, None, None, None).unwrap(); } #[cfg(feature = "config")] @@ -646,6 +697,7 @@ mod tests { None, None, None, + None, ) .unwrap(); assert_eq!( @@ -678,11 +730,12 @@ mod tests { None, None, None, + None, ) .unwrap(); assert_eq!(config.whitespace, Whitespace::Suppress); - let config = Config::new(r#""#, None, None, None, None).unwrap(); + let config = Config::new(r#""#, None, None, None, None, None).unwrap(); assert_eq!(config.whitespace, Whitespace::Preserve); let config = Config::new( @@ -694,6 +747,7 @@ mod tests { None, None, None, + None, ) .unwrap(); assert_eq!(config.whitespace, Whitespace::Preserve); @@ -707,6 +761,7 @@ mod tests { None, None, None, + None, ) .unwrap(); assert_eq!(config.whitespace, Whitespace::Minimize); @@ -727,11 +782,13 @@ mod tests { Some(Whitespace::Minimize), None, None, + None, ) .unwrap(); assert_eq!(config.whitespace, Whitespace::Minimize); - let config = Config::new(r#""#, None, Some(Whitespace::Minimize), None, None).unwrap(); + let config = + Config::new(r#""#, None, Some(Whitespace::Minimize), None, None, None).unwrap(); assert_eq!(config.whitespace, Whitespace::Minimize); } } diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs index e740ec10a..fcede6d3a 100644 --- a/askama_derive/src/generator.rs +++ b/askama_derive/src/generator.rs @@ -5,9 +5,8 @@ mod node; use std::borrow::Cow; use std::collections::hash_map::HashMap; -use std::env::current_dir; use std::ops::Deref; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::str; use std::sync::Arc; @@ -18,7 +17,6 @@ use parser::{ use rustc_hash::FxBuildHasher; use crate::ascii_str::{AsciiChar, AsciiStr}; -use crate::generator::helpers::{clean_path, diff_paths}; use crate::heritage::{Context, Heritage}; use crate::html::write_escaped_str; use crate::input::{Source, TemplateInput}; @@ -89,14 +87,6 @@ struct Generator<'a, 'h> { is_in_filter_block: usize, /// Set of called macros we are currently in. Used to prevent (indirect) recursions. seen_callers: Vec<(&'a Macro<'a>, Option>)>, - /// The directory path of the calling file. - caller_dir: CallerDir, -} - -enum CallerDir { - Valid(PathBuf), - Invalid, - Unresolved, } impl<'a, 'h> Generator<'a, 'h> { @@ -122,42 +112,6 @@ impl<'a, 'h> Generator<'a, 'h> { }, is_in_filter_block, seen_callers: Vec::new(), - caller_dir: CallerDir::Unresolved, - } - } - - fn rel_path<'p>(&mut self, path: &'p Path) -> Cow<'p, Path> { - self.caller_dir() - .and_then(|caller_dir| diff_paths(path, caller_dir)) - .map_or(Cow::Borrowed(path), Cow::Owned) - } - - fn caller_dir(&mut self) -> Option<&Path> { - match self.caller_dir { - CallerDir::Valid(ref caller_dir) => return Some(caller_dir.as_path()), - CallerDir::Invalid => return None, - CallerDir::Unresolved => {} - } - - if proc_macro::is_available() - && let Some(mut local_file) = proc_macro::Span::call_site().local_file() - { - local_file.pop(); - if !local_file.is_absolute() { - local_file = current_dir() - .as_deref() - .unwrap_or(Path::new(".")) - .join(local_file); - } - - self.caller_dir = CallerDir::Valid(clean_path(&local_file)); - match &self.caller_dir { - CallerDir::Valid(caller_dir) => Some(caller_dir.as_path()), - _ => None, // unreachable - } - } else { - self.caller_dir = CallerDir::Invalid; - None } } @@ -195,7 +149,7 @@ impl<'a, 'h> Generator<'a, 'h> { buf.write(format_args!( "const _: &[askama::helpers::core::primitive::u8] =\ askama::helpers::core::include_bytes!({:?});", - self.rel_path(full_config_path).display() + self.input.config.rel_path(full_config_path).display() )); } @@ -217,7 +171,7 @@ impl<'a, 'h> Generator<'a, 'h> { buf.write(format_args!( "const _: &[askama::helpers::core::primitive::u8] =\ askama::helpers::core::include_bytes!({:?});", - self.rel_path(path).display() + self.input.config.rel_path(path).display() )); } } diff --git a/askama_derive/src/generator/helpers/mod.rs b/askama_derive/src/generator/helpers/mod.rs index f24e90dfd..d6b7ec05b 100644 --- a/askama_derive/src/generator/helpers/mod.rs +++ b/askama_derive/src/generator/helpers/mod.rs @@ -1,5 +1,3 @@ mod macro_invocation; -mod paths; pub(crate) use macro_invocation::MacroInvocation; -pub(crate) use paths::{clean as clean_path, diff_paths}; diff --git a/askama_derive/src/input.rs b/askama_derive/src/input.rs index 068f4d445..d531b0c3c 100644 --- a/askama_derive/src/input.rs +++ b/askama_derive/src/input.rs @@ -1131,7 +1131,7 @@ const JINJA_EXTENSIONS: &[&str] = &["askama", "j2", "jinja", "jinja2", "rinja"]; #[test] #[cfg(feature = "external-sources")] fn get_source() { - let path = Config::new("", None, None, None, None) + let path = Config::new("", None, None, None, None, None) .and_then(|config| config.find_template("b.html", None, None)) .unwrap(); assert_eq!(get_template_source(&path, None).unwrap(), "bar".into()); diff --git a/askama_derive/src/integration.rs b/askama_derive/src/integration.rs index 108e39fb7..7b0222ddd 100644 --- a/askama_derive/src/integration.rs +++ b/askama_derive/src/integration.rs @@ -1,4 +1,5 @@ use std::fmt::{Arguments, Display, Write}; +use std::path::Path; use parser::{PathComponent, WithSpan}; use proc_macro2::{TokenStream, TokenTree}; @@ -251,6 +252,7 @@ fn string_escape(dest: &mut String, src: &str) { pub(crate) fn build_template_enum( buf: &mut Buffer, enum_ast: &DeriveInput, + caller_dir: Option<&Path>, mut enum_args: Option, vars_args: Vec>, has_default_impl: bool, @@ -299,6 +301,7 @@ pub(crate) fn build_template_enum( let size_hint = biggest_size_hint.max(build_template_item( buf, &var_ast, + caller_dir, Some(enum_ast), &TemplateArgs::from_partial(&var_ast, Some(var_args))?, TmplKind::Variant, @@ -317,6 +320,7 @@ pub(crate) fn build_template_enum( let size_hint = build_template_item( buf, enum_ast, + caller_dir, None, &TemplateArgs::from_partial(enum_ast, enum_args)?, TmplKind::Variant, diff --git a/askama_derive/src/lib.rs b/askama_derive/src/lib.rs index d06ec04fa..51174fc06 100644 --- a/askama_derive/src/lib.rs +++ b/askama_derive/src/lib.rs @@ -10,6 +10,7 @@ mod heritage; mod html; mod input; mod integration; +mod paths; #[cfg(test)] mod tests; @@ -23,6 +24,7 @@ pub mod __macro_support { use std::borrow::{Borrow, Cow}; use std::collections::hash_map::{Entry, HashMap}; +use std::env::current_dir; use std::fmt; use std::hash::{BuildHasher, Hash}; use std::path::Path; @@ -293,7 +295,7 @@ pub fn derive_template(input: TokenStream, import_askama: fn() -> TokenStream) - fn build_skeleton(buf: &mut Buffer, ast: &syn::DeriveInput) -> Result { let template_args = TemplateArgs::fallback(); - let config = Config::new("", None, None, None, None)?; + let config = Config::new("", None, None, None, None, None)?; let input = TemplateInput::new(ast, None, config, &template_args)?; let mut contexts = HashMap::default(); let parsed = parser::Parsed::default(); @@ -313,6 +315,22 @@ pub(crate) fn build_template( ast: &syn::DeriveInput, args: AnyTemplateArgs, ) -> Result { + let caller_dir = if cfg!(feature = "external-sources") + && proc_macro::is_available() + && let Some(mut local_file) = proc_macro::Span::call_site().local_file() + { + local_file.pop(); + if !local_file.is_absolute() { + local_file = current_dir() + .as_deref() + .unwrap_or(Path::new(".")) + .join(local_file); + } + Some(crate::paths::clean(&local_file)) + } else { + None + }; + let err_span; let mut result = match args { AnyTemplateArgs::Struct(item) => { @@ -322,7 +340,14 @@ pub(crate) fn build_template( .as_ref() .map(|l| l.span()) .or(item.template_span); - build_template_item(buf, ast, None, &item, TmplKind::Struct) + build_template_item( + buf, + ast, + caller_dir.as_deref(), + None, + &item, + TmplKind::Struct, + ) } AnyTemplateArgs::Enum { enum_args, @@ -334,7 +359,14 @@ pub(crate) fn build_template( .and_then(|v| v.source.as_ref()) .map(|s| s.span()) .or_else(|| enum_args.as_ref().map(|v| v.template.span())); - build_template_enum(buf, ast, enum_args, vars_args, has_default_impl) + build_template_enum( + buf, + ast, + caller_dir.as_deref(), + enum_args, + vars_args, + has_default_impl, + ) } }; if let Err(err) = &mut result @@ -348,6 +380,7 @@ pub(crate) fn build_template( fn build_template_item( buf: &mut Buffer, ast: &syn::DeriveInput, + caller_dir: Option<&Path>, enum_ast: Option<&syn::DeriveInput>, template_args: &TemplateArgs, tmpl_kind: TmplKind<'_>, @@ -359,6 +392,7 @@ fn build_template_item( config_path, template_args.whitespace, template_args.config_span, + caller_dir, full_config_path, )?; let input = TemplateInput::new(ast, enum_ast, config, template_args)?; diff --git a/askama_derive/src/generator/helpers/paths.rs b/askama_derive/src/paths.rs similarity index 100% rename from askama_derive/src/generator/helpers/paths.rs rename to askama_derive/src/paths.rs diff --git a/book/src/configuration.md b/book/src/configuration.md index 55bd14847..fd7ec1076 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -9,7 +9,8 @@ This example file demonstrates the default configuration: ```toml [general] -# Directories to search for templates, relative to the crate root. +# Directories to search for templates, relative to the crate root, +# i.e. next to your `Cargo.toml`. dirs = ["templates"] # Unless you add a `-` in a block, whitespace characters won't be trimmed. whitespace = "preserve" diff --git a/book/src/template_syntax.md b/book/src/template_syntax.md index ffdbb65c6..36396c242 100644 --- a/book/src/template_syntax.md +++ b/book/src/template_syntax.md @@ -329,6 +329,16 @@ you want to call a function, you will need to use a path instead: {{ super::b::f() }} ``` +## Path resolution + +In [`{% extends %}`](#template-inheritance), [`{% import %}`](#imports--scopes) and +[`{% include %}`](#include) blocks you interact with code in other template files, so you must specify its path. +The path must be a string literal, so that it is known at compile time. +Askama will try to find the specified template +1. relative to the including template's path +2. in the directories specified in the [configuration](configuration.md) +3. in the folder of the file that contains the `#[derive(Template)]` annotation. + ## Template inheritance Template inheritance allows you to build a base template with common @@ -387,23 +397,13 @@ Here's an example child template: {% endblock %} ``` -The `extends` tag tells the code generator that this template inherits -from another template. It will search for the base template relative to -itself before looking relative to the template base directory. It will -render the top-level content from the base template, and substitute +The `extends` tag tells the code generator that this template inherits from another template. +It will render the top-level content from the base template, and substitute blocks from the base template with those from the child template. Inside a block in a child template, the `super()` macro can be called to render the parent block's contents. -Because top-level content from the child template is thus ignored, the `extends` -tag doesn't support whitespace control: - -```html -{%- extends "base.html" +%} -``` - -The above code is rejected because we used `-` and `+`. For more information -about whitespace control, take a look [here](#whitespace-control). +Please refer to the section [path resolution](#path-resolution) on where askama searches for the base template file. ### Block fragments @@ -696,11 +696,7 @@ in which they're used, including local variables like those from loops: * Item: {{ i }} ``` -The path to include must be a string literal, so that it is known at -compile time. Askama will try to find the specified template relative -to the including template's path before falling back to the absolute -template path. Use `include` within the branches of an `if`/`else` -block to use includes more dynamically. +Please refer to the section [path resolution](#path-resolution) on where askama searches for the included file. ## Expressions @@ -880,6 +876,8 @@ To have a small library of reusable snippets, it's best to declare the macros in {{ scope::heading("test") }} ``` +Please refer to the section [path resolution](#path-resolution) on where askama searches for the imported file. + ### Named Arguments Additionally to specifying arguments positionally, you can also pass arguments by name. This allows passing the arguments in any order: diff --git a/testing/templates/lookup-configured-then-same-dir.html b/testing/templates/lookup-configured-then-same-dir.html new file mode 100644 index 000000000..e31924d3e --- /dev/null +++ b/testing/templates/lookup-configured-then-same-dir.html @@ -0,0 +1 @@ +{% include "lookup-include-same-dir.html" %} {% include "lookup/include-subdir.html" %} diff --git a/testing/tests/lookup-include-same-dir.html b/testing/tests/lookup-include-same-dir.html new file mode 100644 index 000000000..d0653591d --- /dev/null +++ b/testing/tests/lookup-include-same-dir.html @@ -0,0 +1 @@ +(3) Hello world diff --git a/testing/tests/lookup-same-path.html b/testing/tests/lookup-same-path.html new file mode 100644 index 000000000..14fd3025d --- /dev/null +++ b/testing/tests/lookup-same-path.html @@ -0,0 +1 @@ +(1) Hello world in same path. diff --git a/testing/tests/lookup.rs b/testing/tests/lookup.rs new file mode 100644 index 000000000..93d8edc78 --- /dev/null +++ b/testing/tests/lookup.rs @@ -0,0 +1,37 @@ +use askama::Template; + +#[test] +fn test_same_dir() { + // Templates should be searched for in directory path of the declaration. + + #[derive(Template)] + #[template(path = "lookup-same-path.html")] + struct HelloWorld; + + assert_eq!(HelloWorld.to_string(), "(1) Hello world in same path."); +} + +#[test] +fn test_subdir() { + // Templates should be searched for in the subdirectory of the caller location. + + #[derive(Template)] + #[template(path = "lookup/subdir.html")] + struct HelloWorld; + + assert_eq!(HelloWorld.to_string(), "(2) Hello world in relative path."); +} + +#[test] +fn test_configured_then_same_dir() { + // The caller directory should be accessible even when called from configured directories. + + #[derive(Template)] + #[template(path = "lookup-configured-then-same-dir.html")] + struct HelloWorld; + + assert_eq!( + HelloWorld.to_string(), + "(3) Hello world in configured paths, then same dir." + ); +} diff --git a/testing/tests/lookup/include-subdir.html b/testing/tests/lookup/include-subdir.html new file mode 100644 index 000000000..0dfa83729 --- /dev/null +++ b/testing/tests/lookup/include-subdir.html @@ -0,0 +1 @@ +in configured paths, then same dir. diff --git a/testing/tests/lookup/subdir.html b/testing/tests/lookup/subdir.html new file mode 100644 index 000000000..5e8acc084 --- /dev/null +++ b/testing/tests/lookup/subdir.html @@ -0,0 +1 @@ +(2) Hello world in relative path.