diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/luarocks/manif/writer.tl (renamed from src/luarocks/manif/writer.lua) | 157 |
1 files changed, 59 insertions, 98 deletions
diff --git a/src/luarocks/manif/writer.lua b/src/luarocks/manif/writer.tl index 36f5f57f..b333c36f 100644 --- a/src/luarocks/manif/writer.lua +++ b/src/luarocks/manif/writer.tl | |||
@@ -1,5 +1,6 @@ | |||
1 | 1 | ||
2 | local writer = {} | 2 | local record writer |
3 | end | ||
3 | 4 | ||
4 | local cfg = require("luarocks.core.cfg") | 5 | local cfg = require("luarocks.core.cfg") |
5 | local search = require("luarocks.search") | 6 | local search = require("luarocks.search") |
@@ -15,6 +16,17 @@ local persist = require("luarocks.persist") | |||
15 | local manif = require("luarocks.manif") | 16 | local manif = require("luarocks.manif") |
16 | local queries = require("luarocks.queries") | 17 | local queries = require("luarocks.queries") |
17 | 18 | ||
19 | local type Manifest = require("luarocks.core.types.manifest").Manifest | ||
20 | |||
21 | local type Rockspec = require("luarocks.core.types.rockspec").Rockspec | ||
22 | |||
23 | local type Result = require("luarocks.core.types.result").Result | ||
24 | |||
25 | local type PersistableTable = require("luarocks.core.types.persist").PersistableTable | ||
26 | |||
27 | local type RockManifest = require("luarocks.core.types.rockmanifest").RockManifest | ||
28 | local type Entry = require("luarocks.core.types.rockmanifest").RockManifest.Entry | ||
29 | |||
18 | --- Update storage table to account for items provided by a package. | 30 | --- Update storage table to account for items provided by a package. |
19 | -- @param storage table: a table storing items in the following format: | 31 | -- @param storage table: a table storing items in the following format: |
20 | -- keys are item names and values are arrays of packages providing each item, | 32 | -- keys are item names and values are arrays of packages providing each item, |
@@ -22,15 +34,12 @@ local queries = require("luarocks.queries") | |||
22 | -- @param items table: a table mapping item names to paths. | 34 | -- @param items table: a table mapping item names to paths. |
23 | -- @param name string: package name. | 35 | -- @param name string: package name. |
24 | -- @param version string: package version. | 36 | -- @param version string: package version. |
25 | local function store_package_items(storage, name, version, items) | 37 | local function store_package_items(storage: {string: {string}}, name: string, version: string, items: {string: string}) |
26 | assert(type(storage) == "table") | 38 | assert(not name:match("/")) |
27 | assert(type(items) == "table") | ||
28 | assert(type(name) == "string" and not name:match("/")) | ||
29 | assert(type(version) == "string") | ||
30 | 39 | ||
31 | local package_identifier = name.."/"..version | 40 | local package_identifier = name.."/"..version |
32 | 41 | ||
33 | for item_name, path in pairs(items) do -- luacheck: ignore 431 | 42 | for item_name, _ in pairs(items) do -- luacheck: ignore 431 |
34 | if not storage[item_name] then | 43 | if not storage[item_name] then |
35 | storage[item_name] = {} | 44 | storage[item_name] = {} |
36 | end | 45 | end |
@@ -46,15 +55,12 @@ end | |||
46 | -- @param items table: a table mapping item names to paths. | 55 | -- @param items table: a table mapping item names to paths. |
47 | -- @param name string: package name. | 56 | -- @param name string: package name. |
48 | -- @param version string: package version. | 57 | -- @param version string: package version. |
49 | local function remove_package_items(storage, name, version, items) | 58 | local function remove_package_items(storage: {string: {string}}, name: string, version: string, items: {string: string}) |
50 | assert(type(storage) == "table") | 59 | assert(not name:match("/")) |
51 | assert(type(items) == "table") | ||
52 | assert(type(name) == "string" and not name:match("/")) | ||
53 | assert(type(version) == "string") | ||
54 | 60 | ||
55 | local package_identifier = name.."/"..version | 61 | local package_identifier = name.."/"..version |
56 | 62 | ||
57 | for item_name, path in pairs(items) do -- luacheck: ignore 431 | 63 | for item_name, _path in pairs(items) do |
58 | local key = item_name | 64 | local key = item_name |
59 | local all_identifiers = storage[key] | 65 | local all_identifiers = storage[key] |
60 | if not all_identifiers then | 66 | if not all_identifiers then |
@@ -87,9 +93,7 @@ end | |||
87 | -- @param deps_mode string: Dependency mode: "one" for the current default tree, | 93 | -- @param deps_mode string: Dependency mode: "one" for the current default tree, |
88 | -- "all" for all trees, "order" for all trees with priority >= the current default, | 94 | -- "all" for all trees, "order" for all trees with priority >= the current default, |
89 | -- "none" for no trees. | 95 | -- "none" for no trees. |
90 | local function update_dependencies(manifest, deps_mode) | 96 | local function update_dependencies(manifest: Manifest, deps_mode: string) |
91 | assert(type(manifest) == "table") | ||
92 | assert(type(deps_mode) == "string") | ||
93 | 97 | ||
94 | if not manifest.dependencies then manifest.dependencies = {} end | 98 | if not manifest.dependencies then manifest.dependencies = {} end |
95 | local mdeps = manifest.dependencies | 99 | local mdeps = manifest.dependencies |
@@ -117,10 +121,7 @@ end | |||
117 | -- @param b string: Version to compare. | 121 | -- @param b string: Version to compare. |
118 | -- @return boolean: The comparison result, according to the | 122 | -- @return boolean: The comparison result, according to the |
119 | -- rule outlined above. | 123 | -- rule outlined above. |
120 | local function sort_pkgs(a, b) | 124 | local function sort_pkgs(a: string, b: string): boolean |
121 | assert(type(a) == "string") | ||
122 | assert(type(b) == "string") | ||
123 | |||
124 | local na, va = a:match("(.*)/(.*)$") | 125 | local na, va = a:match("(.*)/(.*)$") |
125 | local nb, vb = b:match("(.*)/(.*)$") | 126 | local nb, vb = b:match("(.*)/(.*)$") |
126 | 127 | ||
@@ -130,15 +131,14 @@ end | |||
130 | --- Sort items of a package matching table by version number (higher versions first). | 131 | --- Sort items of a package matching table by version number (higher versions first). |
131 | -- @param tbl table: the package matching table: keys should be strings | 132 | -- @param tbl table: the package matching table: keys should be strings |
132 | -- and values arrays of strings with packages names in "name/version" format. | 133 | -- and values arrays of strings with packages names in "name/version" format. |
133 | local function sort_package_matching_table(tbl) | 134 | local function sort_package_matching_table(tbl: {string: {string}}) |
134 | assert(type(tbl) == "table") | ||
135 | 135 | ||
136 | if next(tbl) then | 136 | if next(tbl) then |
137 | for item, pkgs in pairs(tbl) do | 137 | for item, pkgs in pairs(tbl) do |
138 | if #pkgs > 1 then | 138 | if #pkgs > 1 then |
139 | table.sort(pkgs, sort_pkgs) | 139 | table.sort(pkgs, sort_pkgs) |
140 | -- Remove duplicates from the sorted array. | 140 | -- Remove duplicates from the sorted array. |
141 | local prev = nil | 141 | local prev: string = nil |
142 | local i = 1 | 142 | local i = 1 |
143 | while pkgs[i] do | 143 | while pkgs[i] do |
144 | local curr = pkgs[i] | 144 | local curr = pkgs[i] |
@@ -160,26 +160,24 @@ end | |||
160 | -- @param lua_version string or nil: filter by Lua version | 160 | -- @param lua_version string or nil: filter by Lua version |
161 | -- @param repodir string: directory of repository being scanned | 161 | -- @param repodir string: directory of repository being scanned |
162 | -- @param cache table: temporary rockspec cache table | 162 | -- @param cache table: temporary rockspec cache table |
163 | local function filter_by_lua_version(manifest, lua_version, repodir, cache) | 163 | local function filter_by_lua_version(manifest: Manifest, lua_version_str: string, repodir: string, cache: {string: Rockspec}) |
164 | assert(type(manifest) == "table") | ||
165 | assert(type(repodir) == "string") | ||
166 | assert((not cache) or type(cache) == "table") | ||
167 | 164 | ||
168 | cache = cache or {} | 165 | cache = cache or {} |
169 | lua_version = vers.parse_version(lua_version) | 166 | local lua_version = vers.parse_version(lua_version_str) |
170 | for pkg, versions in pairs(manifest.repository) do | 167 | for pkg, versions in pairs(manifest.repository) do |
171 | local to_remove = {} | 168 | local to_remove: {string} = {} |
172 | for version, repositories in pairs(versions) do | 169 | for version, repositories in pairs(versions) do |
173 | for _, repo in ipairs(repositories) do | 170 | for _, repo in ipairs(repositories) do |
174 | if repo.arch == "rockspec" then | 171 | if repo.arch == "rockspec" then |
175 | local pathname = dir.path(repodir, pkg.."-"..version..".rockspec") | 172 | local pathname = dir.path(repodir, pkg.."-"..version..".rockspec") |
176 | local rockspec, err = cache[pathname] | 173 | local rockspec = cache[pathname] |
174 | local err: string | ||
177 | if not rockspec then | 175 | if not rockspec then |
178 | rockspec, err = fetch.load_local_rockspec(pathname, true) | 176 | rockspec, err = fetch.load_local_rockspec(pathname, true) |
179 | end | 177 | end |
180 | if rockspec then | 178 | if rockspec then |
181 | cache[pathname] = rockspec | 179 | cache[pathname] = rockspec |
182 | for _, dep in ipairs(rockspec.dependencies) do | 180 | for _, dep in ipairs(rockspec.dependencies.queries) do |
183 | if dep.name == "lua" then | 181 | if dep.name == "lua" then |
184 | if not vers.match_constraints(lua_version, dep.constraints) then | 182 | if not vers.match_constraints(lua_version, dep.constraints) then |
185 | table.insert(to_remove, version) | 183 | table.insert(to_remove, version) |
@@ -209,16 +207,14 @@ end | |||
209 | -- @param manifest table: A manifest table (must contain repository, modules, commands tables). | 207 | -- @param manifest table: A manifest table (must contain repository, modules, commands tables). |
210 | -- It will be altered to include the search results. | 208 | -- It will be altered to include the search results. |
211 | -- @return boolean or (nil, string): true in case of success, or nil followed by an error message. | 209 | -- @return boolean or (nil, string): true in case of success, or nil followed by an error message. |
212 | local function store_results(results, manifest) | 210 | local function store_results(results: {string: {string: {Result}}}, manifest: Manifest): boolean, string |
213 | assert(type(results) == "table") | ||
214 | assert(type(manifest) == "table") | ||
215 | 211 | ||
216 | for name, versions in pairs(results) do | 212 | for name, versions in pairs(results) do |
217 | local pkgtable = manifest.repository[name] or {} | 213 | local pkgtable = manifest.repository[name] or {} |
218 | for version, entries in pairs(versions) do | 214 | for version, entries in pairs(versions) do |
219 | local versiontable = {} | 215 | local versiontable = {} |
220 | for _, entry in ipairs(entries) do | 216 | for _, entry in ipairs(entries) do |
221 | local entrytable = {} | 217 | local entrytable: Manifest.Entry = {} |
222 | entrytable.arch = entry.arch | 218 | entrytable.arch = entry.arch |
223 | if entry.arch == "installed" then | 219 | if entry.arch == "installed" then |
224 | local rock_manifest, err = manif.load_rock_manifest(name, version) | 220 | local rock_manifest, err = manif.load_rock_manifest(name, version) |
@@ -246,10 +242,8 @@ end | |||
246 | -- @param tbl table: The table to be saved. | 242 | -- @param tbl table: The table to be saved. |
247 | -- @return boolean or (nil, string): true if successful, or nil and a | 243 | -- @return boolean or (nil, string): true if successful, or nil and a |
248 | -- message in case of errors. | 244 | -- message in case of errors. |
249 | local function save_table(where, name, tbl) | 245 | local function save_table(where: string, name: string, tbl: PersistableTable): boolean, string |
250 | assert(type(where) == "string") | 246 | assert(not name:match("/")) |
251 | assert(type(name) == "string" and not name:match("/")) | ||
252 | assert(type(tbl) == "table") | ||
253 | 247 | ||
254 | local filename = dir.path(where, name) | 248 | local filename = dir.path(where, name) |
255 | local ok, err = persist.save_from_table(filename..".tmp", tbl) | 249 | local ok, err = persist.save_from_table(filename..".tmp", tbl) |
@@ -259,25 +253,28 @@ local function save_table(where, name, tbl) | |||
259 | return ok, err | 253 | return ok, err |
260 | end | 254 | end |
261 | 255 | ||
262 | function writer.make_rock_manifest(name, version) | 256 | function writer.make_rock_manifest(name: string, version: string): boolean, string |
263 | local install_dir = path.install_dir(name, version) | 257 | local install_dir = path.install_dir(name, version) |
264 | local tree = {} | 258 | local tree: {string: Entry} = {} |
265 | for _, file in ipairs(fs.find(install_dir)) do | 259 | for _, file in ipairs(fs.find(install_dir)) do |
266 | local full_path = dir.path(install_dir, file) | 260 | local full_path = dir.path(install_dir, file) |
267 | local walk = tree | 261 | local walk = tree |
268 | local last | 262 | local last: {string : Entry} |
269 | local last_name | 263 | local last_name: string |
264 | local next: Entry | ||
270 | for filename in file:gmatch("[^\\/]+") do | 265 | for filename in file:gmatch("[^\\/]+") do |
271 | local next = walk[filename] | 266 | next = walk[filename] |
272 | if not next then | 267 | if not next then |
273 | next = {} | 268 | next = {} |
274 | walk[filename] = next | 269 | walk[filename] = next |
275 | end | 270 | end |
276 | last = walk | 271 | last = walk |
277 | last_name = filename | 272 | last_name = filename |
273 | assert(next is {string: Entry}) | ||
278 | walk = next | 274 | walk = next |
279 | end | 275 | end |
280 | if fs.is_file(full_path) then | 276 | if fs.is_file(full_path) then |
277 | |||
281 | local sum, err = fs.get_md5(full_path) | 278 | local sum, err = fs.get_md5(full_path) |
282 | if not sum then | 279 | if not sum then |
283 | return nil, "Failed producing checksum: "..tostring(err) | 280 | return nil, "Failed producing checksum: "..tostring(err) |
@@ -285,9 +282,9 @@ function writer.make_rock_manifest(name, version) | |||
285 | last[last_name] = sum | 282 | last[last_name] = sum |
286 | end | 283 | end |
287 | end | 284 | end |
288 | local rock_manifest = { rock_manifest=tree } | 285 | local rock_manifest: RockManifest = { rock_manifest=tree } |
289 | manif.rock_manifest_cache[name.."/"..version] = rock_manifest | 286 | manif.rock_manifest_cache[name.."/"..version] = rock_manifest |
290 | save_table(install_dir, "rock_manifest", rock_manifest ) | 287 | save_table(install_dir, "rock_manifest", rock_manifest as PersistableTable) |
291 | return true | 288 | return true |
292 | end | 289 | end |
293 | 290 | ||
@@ -297,10 +294,8 @@ end | |||
297 | -- @param namespace string?: the namespace | 294 | -- @param namespace string?: the namespace |
298 | -- @return true if successful (or unnecessary, if there is no namespace), | 295 | -- @return true if successful (or unnecessary, if there is no namespace), |
299 | -- or nil and an error message. | 296 | -- or nil and an error message. |
300 | function writer.make_namespace_file(name, version, namespace) | 297 | function writer.make_namespace_file(name: string, version: string, namespace?: string): boolean, string |
301 | assert(type(name) == "string" and not name:match("/")) | 298 | assert(not name:match("/")) |
302 | assert(type(version) == "string") | ||
303 | assert(type(namespace) == "string" or not namespace) | ||
304 | if not namespace then | 299 | if not namespace then |
305 | return true | 300 | return true |
306 | end | 301 | end |
@@ -326,9 +321,7 @@ end | |||
326 | -- @param remote boolean: 'true' if making a manifest for a rocks server. | 321 | -- @param remote boolean: 'true' if making a manifest for a rocks server. |
327 | -- @return boolean or (nil, string): True if manifest was generated, | 322 | -- @return boolean or (nil, string): True if manifest was generated, |
328 | -- or nil and an error message. | 323 | -- or nil and an error message. |
329 | function writer.make_manifest(repo, deps_mode, remote) | 324 | function writer.make_manifest(repo: string, deps_mode: string, remote?: boolean): boolean, string |
330 | assert(type(repo) == "string") | ||
331 | assert(type(deps_mode) == "string") | ||
332 | 325 | ||
333 | if deps_mode == "none" then deps_mode = cfg.deps_mode end | 326 | if deps_mode == "none" then deps_mode = cfg.deps_mode end |
334 | 327 | ||
@@ -349,10 +342,10 @@ function writer.make_manifest(repo, deps_mode, remote) | |||
349 | local cache = {} | 342 | local cache = {} |
350 | for luaver in util.lua_versions() do | 343 | for luaver in util.lua_versions() do |
351 | local vmanifest = { repository = {}, modules = {}, commands = {} } | 344 | local vmanifest = { repository = {}, modules = {}, commands = {} } |
352 | local ok, err = store_results(results, vmanifest) | 345 | ok, err = store_results(results, vmanifest) |
353 | filter_by_lua_version(vmanifest, luaver, repo, cache) | 346 | filter_by_lua_version(vmanifest, luaver, repo, cache) |
354 | if not cfg.no_manifest then | 347 | if not cfg.no_manifest then |
355 | save_table(repo, "manifest-"..luaver, vmanifest) | 348 | save_table(repo, "manifest-"..luaver, vmanifest as PersistableTable) |
356 | end | 349 | end |
357 | end | 350 | end |
358 | else | 351 | else |
@@ -363,7 +356,7 @@ function writer.make_manifest(repo, deps_mode, remote) | |||
363 | -- We want to have cache updated; but exit before save_table is called | 356 | -- We want to have cache updated; but exit before save_table is called |
364 | return true | 357 | return true |
365 | end | 358 | end |
366 | return save_table(repo, "manifest", manifest) | 359 | return save_table(repo, "manifest", manifest as PersistableTable) |
367 | end | 360 | end |
368 | 361 | ||
369 | --- Update manifest file for a local repository | 362 | --- Update manifest file for a local repository |
@@ -377,11 +370,9 @@ end | |||
377 | -- "none" for using the default dependency mode from the configuration. | 370 | -- "none" for using the default dependency mode from the configuration. |
378 | -- @return boolean or (nil, string): True if manifest was updated successfully, | 371 | -- @return boolean or (nil, string): True if manifest was updated successfully, |
379 | -- or nil and an error message. | 372 | -- or nil and an error message. |
380 | function writer.add_to_manifest(name, version, repo, deps_mode) | 373 | function writer.add_to_manifest(name: string, version: string, repo: string, deps_mode: string): boolean, string |
381 | assert(type(name) == "string" and not name:match("/")) | 374 | assert(not name:match("/")) |
382 | assert(type(version) == "string") | ||
383 | local rocks_dir = path.rocks_dir(repo or cfg.root_dir) | 375 | local rocks_dir = path.rocks_dir(repo or cfg.root_dir) |
384 | assert(type(deps_mode) == "string") | ||
385 | 376 | ||
386 | if deps_mode == "none" then deps_mode = cfg.deps_mode end | 377 | if deps_mode == "none" then deps_mode = cfg.deps_mode end |
387 | 378 | ||
@@ -394,9 +385,10 @@ function writer.add_to_manifest(name, version, repo, deps_mode) | |||
394 | return writer.make_manifest(rocks_dir, deps_mode) | 385 | return writer.make_manifest(rocks_dir, deps_mode) |
395 | end | 386 | end |
396 | 387 | ||
397 | local results = {[name] = {[version] = {{arch = "installed", repo = rocks_dir}}}} | 388 | local results: {string : {string : {Result}}} = {[name] = {[version] = {{arch = "installed", repo = rocks_dir}}}} |
398 | 389 | ||
399 | local ok, err = store_results(results, manifest) | 390 | local ok: boolean |
391 | ok, err = store_results(results, manifest) --! | ||
400 | if not ok then return nil, err end | 392 | if not ok then return nil, err end |
401 | 393 | ||
402 | update_dependencies(manifest, deps_mode) | 394 | update_dependencies(manifest, deps_mode) |
@@ -404,7 +396,7 @@ function writer.add_to_manifest(name, version, repo, deps_mode) | |||
404 | if cfg.no_manifest then | 396 | if cfg.no_manifest then |
405 | return true | 397 | return true |
406 | end | 398 | end |
407 | return save_table(rocks_dir, "manifest", manifest) | 399 | return save_table(rocks_dir, "manifest", manifest as PersistableTable) |
408 | end | 400 | end |
409 | 401 | ||
410 | --- Update manifest file for a local repository | 402 | --- Update manifest file for a local repository |
@@ -418,11 +410,9 @@ end | |||
418 | -- "none" for using the default dependency mode from the configuration. | 410 | -- "none" for using the default dependency mode from the configuration. |
419 | -- @return boolean or (nil, string): True if manifest was updated successfully, | 411 | -- @return boolean or (nil, string): True if manifest was updated successfully, |
420 | -- or nil and an error message. | 412 | -- or nil and an error message. |
421 | function writer.remove_from_manifest(name, version, repo, deps_mode) | 413 | function writer.remove_from_manifest(name: string, version: string, repo: string, deps_mode: string): boolean, string |
422 | assert(type(name) == "string" and not name:match("/")) | 414 | assert(not name:match("/")) |
423 | assert(type(version) == "string") | ||
424 | local rocks_dir = path.rocks_dir(repo or cfg.root_dir) | 415 | local rocks_dir = path.rocks_dir(repo or cfg.root_dir) |
425 | assert(type(deps_mode) == "string") | ||
426 | 416 | ||
427 | if deps_mode == "none" then deps_mode = cfg.deps_mode end | 417 | if deps_mode == "none" then deps_mode = cfg.deps_mode end |
428 | 418 | ||
@@ -446,7 +436,7 @@ function writer.remove_from_manifest(name, version, repo, deps_mode) | |||
446 | return writer.make_manifest(rocks_dir, deps_mode) | 436 | return writer.make_manifest(rocks_dir, deps_mode) |
447 | end | 437 | end |
448 | 438 | ||
449 | remove_package_items(manifest.modules, name, version, version_entry.modules) | 439 | remove_package_items(manifest.modules, name, version, version_entry.modules) --! |
450 | remove_package_items(manifest.commands, name, version, version_entry.commands) | 440 | remove_package_items(manifest.commands, name, version, version_entry.commands) |
451 | 441 | ||
452 | package_entry[version] = nil | 442 | package_entry[version] = nil |
@@ -463,36 +453,7 @@ function writer.remove_from_manifest(name, version, repo, deps_mode) | |||
463 | if cfg.no_manifest then | 453 | if cfg.no_manifest then |
464 | return true | 454 | return true |
465 | end | 455 | end |
466 | return save_table(rocks_dir, "manifest", manifest) | 456 | return save_table(rocks_dir, "manifest", manifest as PersistableTable) |
467 | end | ||
468 | |||
469 | --- Report missing dependencies for all rocks installed in a repository. | ||
470 | -- @param repo string or nil: Pathname of a local repository. If not given, | ||
471 | -- the default local repository is used. | ||
472 | -- @param deps_mode string: Dependency mode: "one" for the current default tree, | ||
473 | -- "all" for all trees, "order" for all trees with priority >= the current default, | ||
474 | -- "none" for using the default dependency mode from the configuration. | ||
475 | function writer.check_dependencies(repo, deps_mode) | ||
476 | local rocks_dir = path.rocks_dir(repo or cfg.root_dir) | ||
477 | assert(type(deps_mode) == "string") | ||
478 | if deps_mode == "none" then deps_mode = cfg.deps_mode end | ||
479 | |||
480 | local manifest = manif.load_manifest(rocks_dir) | ||
481 | if not manifest then | ||
482 | return | ||
483 | end | ||
484 | |||
485 | for name, versions in util.sortedpairs(manifest.repository) do | ||
486 | for version, version_entries in util.sortedpairs(versions, vers.compare_versions) do | ||
487 | for _, entry in ipairs(version_entries) do | ||
488 | if entry.arch == "installed" then | ||
489 | if manifest.dependencies[name] and manifest.dependencies[name][version] then | ||
490 | deps.report_missing_dependencies(name, version, manifest.dependencies[name][version], deps_mode, util.get_rocks_provided()) | ||
491 | end | ||
492 | end | ||
493 | end | ||
494 | end | ||
495 | end | ||
496 | end | 457 | end |
497 | 458 | ||
498 | return writer | 459 | return writer |