PreviousUpNext

15.4.821  src/lib/make-library-glue/patchfile.pkg

## patchfile.pkg
#
# Adding content to files in spots
# marked by linepairs like
#
#    # Do not edit this or following lines --- they are autobuilt.
#    ...
#    # Do not edit this or preceding lines --- they are autobuilt.

# Compiled by:
#     src/lib/std/standard.lib

stipulate
    package fil =  file__premicrothread;                                                                # file__premicrothread          is from   src/lib/std/src/posix/file--premicrothread.pkg
    package deq =  queue;                                                                               # queue                         is from   src/lib/src/queue.pkg
    package psx =  posixlib;                                                                            # posixlib                      is from   src/lib/std/src/psx/posixlib.pkg
    package sm  =  string_map;                                                                          # string_map                    is from   src/lib/src/string-map.pkg
    #
    =~     =  regex::(=~);
herein

    # This package is invoked in:
    #
    #     src/lib/make-library-glue/make-library-glue.pkg

    package  patchfile:
             Patchfile                                                                                  # Patchfile                     is from   src/lib/make-library-glue/patchfile.api
    {
        # We divide the files we patch into texts and patches
        # according to the scheme
        #
        #        text
        #        # Do not edit this or following lines --- they are autobuilt.
        #        patch
        #        # Do not edit this or preceding lines --- they are autobuilt.
        #        text
        #        
        # where the texts are literal program text provided by the
        # programmer whereas the patches are literal program text
        # which we synthesize.  (The shown 'do not edit' lines are
        # considered part of the texts.)
        #
        # We represent such a file in memory as a list of file
        # parts (that is, texts and patches) where each part
        # is in turn a list of lines represented as strings:
        #

        Patch   = { patchname:          String,                                                         # This is the externally visible representation of a patch,
                    lines:              List(String)                                                    # designed for client-code convenience, mostly used for
                  };                                                                                    # arguments to (and results from) exported functions.

        Patch_Id  =   { filename:       String,
                        patchname:      String
                      };

        Patch'    =   { patch_id:       Patch_Id,
                        lines:          List(String)
                      };

        Patch'' = { patchname:          String,                                                         # This is the internal representation of a patch,
                    deque:              deq::Queue(String)                                              # which allows for more efficient prepend/append operations.
                  };

        File_Part = TEXT  List(String)                                                                  # Static part contents as a list of lines.
                  | PATCH String                                                                        # Name of patch.
                  ;

        Patchfile = PATCHFILE {
                        filename:   String,
                        parts:      List(File_Part),                                                    # This preserves the order of the file parts and the contents of the TEXT parts.
                        patches:    sm::Map(Patch'')                                                    # This preserves the contents of the patches, indexed by name.
                      };


        fun print_patchfile (PATCHFILE { filename, parts, patches })
            =
            {   printf "PATCHFILE filename = '%s'\n" filename;
                printf "PATCHFILE parts follow:\n";
                print_patchfile_parts parts;
            }
            where
                fun print_strings [] => ();
                    print_strings (string ! rest)
                        =>
                        {   print string;
                            print_strings rest;
                        };      
                end;

                fun print_patchfile_parts []
                        =>
                        ();

                    print_patchfile_parts (TEXT strings ! rest)
                        =>
                        {   print "TEXT:\n";
                            print_strings strings;
                            print_patchfile_parts  rest;
                        };

                    print_patchfile_parts (PATCH patchname ! rest)
                        =>
                        case (sm::get (patches, patchname))
                            #
                            THE { patchname, deque }
                                =>
                                {   printf "PATCH '%s':\n" patchname;
                                    print_strings (deq::to_list deque);
                                    print_patchfile_parts  rest;
                                };

                            NULL => raise exception DIE "impossible";
                        esac;
                end;
            end;

        fun patch_count (PATCHFILE { parts, ... })
            =
            patch_count' (parts, 0)
            where
                fun patch_count' ([],             n) =>  n;
                    patch_count' ( TEXT _ ! rest, n) =>  patch_count' (rest, n    );
                    patch_count' (PATCH _ ! rest, n) =>  patch_count' (rest, n + 1);
                end;
            end;

        fun text_count (PATCHFILE { parts, ... })
            =
            text_count' (parts, 0)
            where
                fun text_count' ([],             n) =>  n;
                    text_count' ( TEXT _ ! rest, n) =>  text_count' (rest, n + 1);
                    text_count' (PATCH _ ! rest, n) =>  text_count' (rest, n    );
                end;
            end;

        fun get_patch_names (PATCHFILE { patches, ... })
            =
            sm::keys_list patches;


        stipulate
            fun get (PATCHFILE { filename, patches, ... },  patchname)
                =
                case (sm::get (patches, patchname))
                    #
                    THE patch =>  patch;
                    NULL      =>  raise exception DIE (sprintf "No patch %s in file %s" patchname filename);
                esac;
        herein

            fun get_patch  (pf as PATCHFILE { filename, patches, ... },  patchname)
                =
                {   (get (pf, patchname)) ->  { patchname, deque };
                    #
                    { patchname, lines => deq::to_list deque };
                };


            fun apply_patch  (pf as PATCHFILE { filename, parts, patches })  { patchname, lines }
                =
                {   get (pf, patchname);        # Verify that patch exists.
                    #
                    PATCHFILE  { filename,  parts,  patches => sm::set (patches, patchname, { patchname, deque => deq::from_list lines } ) };
                };


            fun prepend_to_patch  (pf as PATCHFILE { filename, parts, patches },  patchname, lines)
                =
                {   (get (pf, patchname)) ->  { patchname, deque };
                    #
                    PATCHFILE  { filename,  parts,  patches => sm::set (patches, patchname, { patchname, deque => deq::unpull' (deque, lines) } ) };
                };


            fun append_to_patch  (pf as PATCHFILE { filename, parts, patches },  patchname, lines)
                =
                {   (get (pf, patchname)) ->  { patchname, deque };
                    #
                    PATCHFILE  { filename,  parts,  patches => sm::set (patches, patchname, { patchname, deque => deq::push' (deque, lines) } ) };
                };
        end;

        fun get_only_patch (pf as PATCHFILE { filename, parts, patches })
            =
            case (patch_count pf)
                #
                1 =>    get_patch' parts
                        where
                            fun get_patch' []                       =>  raise exception DIE "impossible";
                                get_patch' ( TEXT _         ! rest) =>  get_patch'  rest;
                                get_patch' (PATCH patchname ! _   ) =>  case (sm::get (patches, patchname))
                                                                            #
                                                                            THE { patchname, deque } => deq::to_list deque;
                                                                            NULL => raise exception DIE "impossible";
                                                                        esac;
                            end;
                        end;

                n =>    raise exception DIE (sprintf "get_only_patch: Patchable file %s has %d patches instead of 1" filename n);
            esac;

        fun set_only_patch (pf as PATCHFILE { filename, parts, patches })  lines
            =
            case (patch_count pf)
                #
                1 =>    PATCHFILE { filename, parts, patches => set_patch' parts }
                        where
                            fun set_patch' []                     =>  raise exception DIE "impossible";
                                #
                                set_patch' (TEXT _  ! rest)       =>  set_patch' rest;
                                set_patch' (PATCH patchname ! _)  =>  sm::set (patches, patchname, { patchname, deque => deq::from_list lines } );
                            end;
                        end;

                n =>    raise exception DIE (sprintf "set_only_patch: patchable file %s has %d patches instead of 1" filename n);
            esac;

        fun apply_patches (pf as PATCHFILE { filename, parts, patches })  replacement_patches
            =
            PATCHFILE  { filename,  parts, patches => set_it (replacement_patches, patches) }
            where
                fun set_it ([], patches)                                =>  patches;
                    #
                    set_it (({ patchname, lines }) ! rest,  patches)
                        =>
                        case (sm::get (patches, patchname))
                            #
                            THE _  =>  set_it (rest, sm::set (patches, patchname, { patchname, deque => deq::from_list lines } ));
                            NULL   =>  raise exception DIE "impossible";
                        esac;
                end;
            end;



        fun make_patch_beginline { patchname: String } =  sprintf " Do not edit this or following lines --- they are autobuilt.  (patchname=\"%s\")"  patchname;
        fun make_patch_endline   { patchname: String } =  sprintf " Do not edit this or preceding lines --- they are autobuilt.  (patchname=\"%s\")"  patchname;
            #
            # The point of these two functions is to avoid embedding knowledge
            # about these magic lines in files that generate patchable files.
            #
            # We do not include newlines because caller may need to wrap
            # the lines in (say) /* ... */ style comment characters.  

        # Read and return a Patchfile:
        #
        fun read_patchfile  filename
            =
            {
                fd = fil::open_for_read filename
                     except
                        io_exceptions::IO _
                            =
                            raise exception DIE (sprintf "Fatal: Unable to open input file '%s'" filename);

                my (parts, patches)
                    =
                    read_text ([], sm::empty, [])
                    where
                        fun read_text (parts, patches, lines)
                            =
                            case (fil::read_line  fd)
                                #
                                NULL =>  (reverse ((TEXT (reverse lines)) ! parts),  patches);

                                THE line
                                    =>
                                    if (line =~ ./ Do not edit this or following lines --- they are autobuilt.  \(patchname="[A-Za-z0-9_\-]+"\)/)
                                        #
                                        patchname =  the (regex::find_first_match_to_ith_group 1 ./patchname="([A-Za-z0-9_\-]+)"/  line);       # Removing the \ from the pattern yields a useless 'unable to parse' error message.  XXX SUCKO FIXME.
                                        #
                                        read_patch ((TEXT (reverse (line ! lines))) ! parts, patches, patchname, []);
                                    else
                                        read_text (parts, patches, line ! lines);
                                    fi; 
                            esac

                        also
                        fun read_patch (parts, patches, patchname, lines)
                            =
                            case (fil::read_line  fd)
                                #
                                THE line
                                    =>
                                    if (line =~ ./ Do not edit this or preceding lines --- they are autobuilt./)
                                        #
                                        patches = sm::set (patches, patchname, { patchname, deque => deq::from_list (reverse lines) } );

                                        read_text  ((PATCH patchname) ! parts,  patches,  [ line ]);
                                    else
                                        read_patch (parts, patches, patchname, line ! lines);
                                    fi; 

                                NULL =>     raise exception DIE (sprintf "Fatal: Missing 'Do not edit this or preceding lines' line in %s" filename);
                            esac;
                    end;

                fil::close_input  fd;

                PATCHFILE { filename, parts, patches };
            };

        # Write a patchable file back into the filesystem.
        #
        fun write_patchfile  (PATCHFILE { filename, parts, patches })
            =
            {
                stat = psx::stat  filename;                     # Get original filemode, so that we can create updated copy of file with same mode.

                patch_lines_written =  REF 0;
                #
                tmp_filename = filename + "~";                  # Write updated file to a temporary filename, so that if we
                                                                # crash halfway through the write, the original remains untouched.
                fd = psx::creat (tmp_filename, stat.mode)
                     except
                        _ = raise exception DIE (sprintf "Fatal: Unable to open output file '%s'" tmp_filename);


                fun write_text_lines  (line ! rest)
                        =>
                        {   psx::write_string (fd, line);
                            #
                            write_text_lines  rest;
                        };

                    write_text_lines [] =>  ();
                end;


                fun write_patch_lines  (line ! rest)                                    # Actually 'line' may be just a string (i.e., line fragment).
                        =>
                        {   psx::write_string (fd, line);
                            #
                            patch_lines_written := *patch_lines_written + 1;            # This is the only difference between us and write_text_lines.
                            write_patch_lines rest;
                        };

                    write_patch_lines  [] =>  ();
                end;


                write_text  parts
                where
                    fun write_text ((TEXT lines) ! rest)
                            =>
                            {
                                write_text_lines lines;
                                write_patch rest;
                            };

                        write_text []
                            =>
                            ();

                        write_text _
                            =>
                            raise exception DIE (sprintf "Internal bug detected in write_text in write_patchfile while processing %s" filename);
                    end

                    also
                    fun write_patch ((PATCH patchname) ! rest)
                            =>
                            {
                                case (sm::get (patches, patchname))
                                    #
                                    THE { patchname, deque } =>  write_patch_lines  (deq::to_list  deque);
                                    #
                                    NULL                     =>  raise exception DIE "impossible";
                                esac;
                                
                                write_text rest;
                            };

                        write_patch []
                            =>
                            ();

                        write_patch _
                            =>
                            raise exception DIE (sprintf "Internal bug detected in write_patch in write_patchfile while processing %s" filename);
                    end;
                end;    

                psx::close fd;   

                # To avoid needlessly exciting Make or emacs or such,
                # it is good to avoid overwriting/replacing the original
                # file unless the new one is different:
                #
                if (not  (psx::file_contents_are_identical (filename, tmp_filename)))
                    #
                    winix__premicrothread::file::remove_file  filename;

                    winix__premicrothread::file::rename_file { from => tmp_filename, to => filename };

                    sprintf "Alterations   to file %-50s %4d lines of patches."  (filename+":")  *patch_lines_written;
                else
                    winix__premicrothread::file::remove_file  tmp_filename;

                    sprintf "No net change to file %s." filename;
                fi;
            };


        # Write a patchable file back into the filesystem.
        #
        fun write_patchfile'  patchfile  patches
            =
            {   patchfile =  apply_patches  patchfile  patches;
                #
                write_patchfile  patchfile;
            };



        fun map_patches  user_fn  (pf as PATCHFILE { filename, parts, patches })                                # Transform lines of all patches per user_fn.
            =
            {   patches =   sm::map  map_it  patches
                            where
                                fun map_it { patchname, deque }
                                    =
                                    {   list =  user_fn { patch_id =>  { filename, patchname },                 # Transform from internal to external patch representation and apply user fn.
                                                          lines    =>  deq::to_list deque
                                                        };
                                        #
                                        { patchname, deque => deq::from_list list };                            # Transform back from external to internal patch representation.
                                    };
                            end;        
                
                PATCHFILE { filename, parts, patches };
            };


        fun patch_apply  user_fn  (pf as PATCHFILE { filename, parts, patches })                                # Call user_fn on all patches.
            =
            {   patches =   sm::apply  apply_fn  patches
                            where
                                fun apply_fn { patchname, deque }
                                    =
                                    user_fn    { patch_id =>  { filename, patchname },                          # Transform from internal to external patch representation and apply user fn.
                                                 lines    =>  deq::to_list deque
                                               };
                            end;        
            };


        fun patch_fold   user_fn   init   (patchfile as PATCHFILE { filename, patches, ... })
            =
            sm::fold_backward  patch_fold'  init  patches
            where
                fun patch_fold' ({ patchname, deque }, result)
                    =
                    user_fn ( { patch_id =>  { filename, patchname },
                                lines    =>  deq::to_list deque
                              },
                              result
                            );
            end;


        fun empty_all_patches pf
            =
            map_patches  (\\ _ = [])  pf;


        map   =  map_patches;           # Calling these 'map', 'apply' and 'fold' in the main body of the file risks confusion
        apply =  patch_apply;           # with list::map and list::apply, but exporting them as pf::map etc is nonproblematic.
        fold  =  patch_fold;
    };
end;


## Code by Jeff Prothero: Copyright (c) 2010-2015,
## released per terms of SMLNJ-COPYRIGHT.


Comments and suggestions to: bugs@mythryl.org

PreviousUpNext